Home | History | Annotate | Download | only in conscrypt
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package org.conscrypt;
     18 
     19 import static org.conscrypt.TestUtils.getConscryptProvider;
     20 import static org.conscrypt.TestUtils.getJdkProvider;
     21 import static org.conscrypt.TestUtils.getProtocols;
     22 import static org.conscrypt.TestUtils.initSslContext;
     23 import static org.conscrypt.TestUtils.newTextMessage;
     24 import static org.junit.Assert.assertArrayEquals;
     25 import static org.junit.Assert.assertEquals;
     26 import static org.junit.Assert.assertFalse;
     27 import static org.junit.Assert.assertNull;
     28 import static org.junit.Assert.assertTrue;
     29 import static org.mockito.Matchers.same;
     30 import static org.mockito.Mockito.when;
     31 
     32 import java.io.ByteArrayOutputStream;
     33 import java.io.IOException;
     34 import java.nio.ByteBuffer;
     35 import java.security.NoSuchAlgorithmException;
     36 import java.security.Provider;
     37 import java.util.ArrayList;
     38 import java.util.Arrays;
     39 import java.util.List;
     40 import javax.net.ssl.SSLContext;
     41 import javax.net.ssl.SSLEngine;
     42 import javax.net.ssl.SSLEngineResult;
     43 import javax.net.ssl.SSLEngineResult.HandshakeStatus;
     44 import javax.net.ssl.SSLEngineResult.Status;
     45 import javax.net.ssl.SSLException;
     46 import javax.net.ssl.SSLHandshakeException;
     47 import javax.net.ssl.SSLSession;
     48 import org.conscrypt.java.security.TestKeyStore;
     49 import org.junit.Test;
     50 import org.junit.runner.RunWith;
     51 import org.junit.runners.Parameterized;
     52 import org.junit.runners.Parameterized.Parameter;
     53 import org.junit.runners.Parameterized.Parameters;
     54 import org.mockito.Matchers;
     55 import org.mockito.Mockito;
     56 
     57 @RunWith(Parameterized.class)
     58 public class ConscryptEngineTest {
     59     private static final int MESSAGE_SIZE = 4096;
     60     private static final int LARGE_MESSAGE_SIZE = 16413;
     61     private static final String[] CIPHERS = TestUtils.getCommonCipherSuites();
     62     private static final String RENEGOTIATION_CIPHER = CIPHERS[CIPHERS.length - 1];
     63 
     64     @SuppressWarnings("ImmutableEnumChecker")
     65     public enum BufferType {
     66         HEAP_ALLOCATOR(BufferAllocator.unpooled()) {
     67             @Override
     68             ByteBuffer newBuffer(int size) {
     69                 return ByteBuffer.allocate(size);
     70             }
     71         },
     72         HEAP_NO_ALLOCATOR(null) {
     73             @Override
     74             ByteBuffer newBuffer(int size) {
     75                 return ByteBuffer.allocate(size);
     76             }
     77         },
     78         DIRECT(null) {
     79             @Override
     80             ByteBuffer newBuffer(int size) {
     81                 return ByteBuffer.allocateDirect(size);
     82             }
     83         };
     84 
     85         abstract ByteBuffer newBuffer(int size);
     86 
     87         BufferType(BufferAllocator allocator) {
     88             this.allocator = allocator;
     89         }
     90 
     91         private final BufferAllocator allocator;
     92     }
     93 
     94     private enum ClientAuth {
     95         NONE {
     96             @Override
     97             void apply(SSLEngine engine) {
     98                 engine.setWantClientAuth(false);
     99                 engine.setNeedClientAuth(false);
    100             }
    101         },
    102         OPTIONAL {
    103             @Override
    104             void apply(SSLEngine engine) {
    105                 engine.setWantClientAuth(true);
    106                 engine.setNeedClientAuth(false);
    107             }
    108         },
    109         REQUIRED {
    110             @Override
    111             void apply(SSLEngine engine) {
    112                 engine.setWantClientAuth(false);
    113                 engine.setNeedClientAuth(true);
    114             }
    115         };
    116 
    117         abstract void apply(SSLEngine engine);
    118     }
    119 
    120     @Parameters(name = "{0}")
    121     public static Iterable<BufferType> data() {
    122         return Arrays.asList(
    123                 BufferType.HEAP_ALLOCATOR, BufferType.HEAP_NO_ALLOCATOR, BufferType.DIRECT);
    124     }
    125 
    126     @Parameter public BufferType bufferType;
    127 
    128     private SSLEngine clientEngine;
    129     private SSLEngine serverEngine;
    130 
    131     @Test
    132     public void closingOutboundBeforeHandshakeShouldCloseAll() throws Exception {
    133         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    134         assertFalse(clientEngine.isInboundDone());
    135         assertFalse(clientEngine.isOutboundDone());
    136         assertFalse(serverEngine.isInboundDone());
    137         assertFalse(serverEngine.isOutboundDone());
    138 
    139         clientEngine.closeOutbound();
    140         serverEngine.closeOutbound();
    141 
    142         assertTrue(clientEngine.isInboundDone());
    143         assertTrue(clientEngine.isOutboundDone());
    144         assertTrue(serverEngine.isInboundDone());
    145         assertTrue(serverEngine.isOutboundDone());
    146     }
    147 
    148     @Test
    149     public void closingOutboundAfterHandshakeShouldOnlyCloseOutbound() throws Exception {
    150         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    151         doHandshake(true);
    152 
    153         assertFalse(clientEngine.isInboundDone());
    154         assertFalse(clientEngine.isOutboundDone());
    155         assertFalse(serverEngine.isInboundDone());
    156         assertFalse(serverEngine.isOutboundDone());
    157 
    158         clientEngine.closeOutbound();
    159         serverEngine.closeOutbound();
    160 
    161         assertFalse(clientEngine.isInboundDone());
    162         assertTrue(clientEngine.isOutboundDone());
    163         assertFalse(serverEngine.isInboundDone());
    164         assertTrue(serverEngine.isOutboundDone());
    165     }
    166 
    167     @Test
    168     public void closingInboundShouldOnlyCloseInbound() throws Exception {
    169         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    170         doHandshake(true);
    171 
    172         assertFalse(clientEngine.isInboundDone());
    173         assertFalse(clientEngine.isOutboundDone());
    174         assertFalse(serverEngine.isInboundDone());
    175         assertFalse(serverEngine.isOutboundDone());
    176 
    177         clientEngine.closeInbound();
    178         serverEngine.closeInbound();
    179 
    180         assertTrue(clientEngine.isInboundDone());
    181         assertFalse(clientEngine.isOutboundDone());
    182         assertTrue(serverEngine.isInboundDone());
    183         assertFalse(serverEngine.isOutboundDone());
    184     }
    185 
    186     @Test
    187     public void mutualAuthWithSameCertsShouldSucceed() throws Exception {
    188         doMutualAuthHandshake(TestKeyStore.getServer(), TestKeyStore.getServer(), ClientAuth.NONE);
    189     }
    190 
    191     @Test
    192     public void mutualAuthWithDifferentCertsShouldSucceed() throws Exception {
    193         doMutualAuthHandshake(TestKeyStore.getClient(), TestKeyStore.getServer(), ClientAuth.NONE);
    194     }
    195 
    196     @Test(expected = SSLHandshakeException.class)
    197     public void mutualAuthWithUntrustedServerShouldFail() throws Exception {
    198         doMutualAuthHandshake(
    199                 TestKeyStore.getClientCA2(), TestKeyStore.getServer(), ClientAuth.NONE);
    200     }
    201 
    202     @Test(expected = SSLHandshakeException.class)
    203     public void mutualAuthWithUntrustedClientShouldFail() throws Exception {
    204         doMutualAuthHandshake(TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.NONE);
    205     }
    206 
    207     @Test
    208     public void optionalClientAuthShouldSucceed() throws Exception {
    209         doMutualAuthHandshake(
    210                 TestKeyStore.getClient(), TestKeyStore.getServer(), ClientAuth.OPTIONAL);
    211     }
    212 
    213     @Test(expected = SSLHandshakeException.class)
    214     public void optionalClientAuthShouldFail() throws Exception {
    215         doMutualAuthHandshake(
    216                 TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.OPTIONAL);
    217     }
    218 
    219     @Test
    220     public void requiredClientAuthShouldSucceed() throws Exception {
    221         doMutualAuthHandshake(
    222                 TestKeyStore.getServer(), TestKeyStore.getServer(), ClientAuth.REQUIRED);
    223     }
    224 
    225     @Test(expected = SSLHandshakeException.class)
    226     public void requiredClientAuthShouldFail() throws Exception {
    227         doMutualAuthHandshake(
    228                 TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.REQUIRED);
    229     }
    230 
    231     @Test
    232     public void exchangeMessages() throws Exception {
    233         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    234         doHandshake(true);
    235 
    236         ByteBuffer message = newMessage(MESSAGE_SIZE);
    237         byte[] messageBytes = toArray(message);
    238 
    239         // Wrap the original message and create the encrypted data.
    240         final int numMessages = 100;
    241         ByteBuffer[] encryptedBuffers = new ByteBuffer[numMessages];
    242         for (int i = 0; i < numMessages; ++i) {
    243             List<ByteBuffer> wrapped = wrap(message.duplicate(), clientEngine);
    244             // Small message, we should only have 1 buffer created.
    245             assertEquals(1, wrapped.size());
    246             encryptedBuffers[i] = wrapped.get(0);
    247         }
    248 
    249         // Unwrap the all of the encrypted messages.
    250         byte[] actualBytes = unwrap(encryptedBuffers, serverEngine);
    251         assertEquals(MESSAGE_SIZE * numMessages, actualBytes.length);
    252         for (int i = 0; i < numMessages; ++i) {
    253             int offset = i * MESSAGE_SIZE;
    254             byte[] actualMessageBytes =
    255                     Arrays.copyOfRange(actualBytes, offset, offset + MESSAGE_SIZE);
    256             assertArrayEquals(messageBytes, actualMessageBytes);
    257         }
    258     }
    259 
    260     @Test
    261     public void exchangeLargeMessage() throws Exception {
    262         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    263         doHandshake(true);
    264 
    265         ByteBuffer inputBuffer = newMessage(LARGE_MESSAGE_SIZE);
    266         exchangeMessage(inputBuffer, clientEngine, serverEngine);
    267     }
    268 
    269     @Test
    270     public void alpnWithProtocolListShouldSucceed() throws Exception {
    271         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    272 
    273         // Configure ALPN protocols
    274         String[] clientAlpnProtocols = new String[]{"http/1.1", "foo", "spdy/2"};
    275         String[] serverAlpnProtocols = new String[]{"spdy/2", "foo", "bar"};
    276 
    277         Conscrypt.setApplicationProtocols(clientEngine, clientAlpnProtocols);
    278         Conscrypt.setApplicationProtocols(serverEngine, serverAlpnProtocols);
    279 
    280         doHandshake(true);
    281         assertEquals("spdy/2", Conscrypt.getApplicationProtocol(clientEngine));
    282         assertEquals("spdy/2", Conscrypt.getApplicationProtocol(serverEngine));
    283     }
    284 
    285     @Test
    286     public void alpnWithProtocolListShouldFail() throws Exception {
    287         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    288 
    289         // Configure ALPN protocols
    290         String[] clientAlpnProtocols = new String[]{"http/1.1", "foo", "spdy/2"};
    291         String[] serverAlpnProtocols = new String[]{"h2", "bar", "baz"};
    292 
    293         Conscrypt.setApplicationProtocols(clientEngine, clientAlpnProtocols);
    294         Conscrypt.setApplicationProtocols(serverEngine, serverAlpnProtocols);
    295 
    296         doHandshake(true);
    297         assertNull(Conscrypt.getApplicationProtocol(clientEngine));
    298         assertNull(Conscrypt.getApplicationProtocol(serverEngine));
    299     }
    300 
    301     @Test
    302     public void alpnWithServerProtocolSelectorShouldSucceed() throws Exception {
    303         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    304 
    305         // Configure client protocols.
    306         String[] clientAlpnProtocols = new String[]{"http/1.1", "foo", "spdy/2"};
    307         Conscrypt.setApplicationProtocols(clientEngine, clientAlpnProtocols);
    308 
    309         // Configure server selector
    310         ApplicationProtocolSelector selector = Mockito.mock(ApplicationProtocolSelector.class);
    311         when(selector.selectApplicationProtocol(same(serverEngine), Matchers.anyListOf(String.class)))
    312                 .thenReturn("spdy/2");
    313         Conscrypt.setApplicationProtocolSelector(serverEngine, selector);
    314 
    315         doHandshake(true);
    316         assertEquals("spdy/2", Conscrypt.getApplicationProtocol(clientEngine));
    317         assertEquals("spdy/2", Conscrypt.getApplicationProtocol(serverEngine));
    318     }
    319 
    320     @Test
    321     public void alpnWithServerProtocolSelectorShouldFail() throws Exception {
    322         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    323 
    324         // Configure client protocols.
    325         String[] clientAlpnProtocols = new String[]{"http/1.1", "foo", "spdy/2"};
    326         Conscrypt.setApplicationProtocols(clientEngine, clientAlpnProtocols);
    327 
    328         // Configure server selector
    329         ApplicationProtocolSelector selector = Mockito.mock(ApplicationProtocolSelector.class);
    330         when(selector.selectApplicationProtocol(same(serverEngine), Matchers.anyListOf(String.class)))
    331                 .thenReturn("h2");
    332         Conscrypt.setApplicationProtocolSelector(serverEngine, selector);
    333 
    334         doHandshake(true);
    335         assertNull(Conscrypt.getApplicationProtocol(clientEngine));
    336         assertNull(Conscrypt.getApplicationProtocol(serverEngine));
    337     }
    338 
    339     /**
    340      * BoringSSL server doesn't support renegotiation. BoringSSL clients do not support
    341      * initiating a renegotiation, only processing a renegotiation initiated by the
    342      * (non-BoringSSL) server. For this reason we test a server-initiated renegotiation with
    343      * a Conscrypt client and a JDK server.
    344      */
    345     @Test
    346     public void serverInitiatedRenegotiationShouldSucceed() throws Exception {
    347         setupClientEngine(getConscryptProvider(), TestKeyStore.getClient());
    348         setupServerEngine(getJdkProvider(), TestKeyStore.getServer());
    349 
    350         // Perform the initial handshake.
    351         doHandshake(true);
    352 
    353         // Send a message from client->server.
    354         exchangeMessage(newMessage(MESSAGE_SIZE), clientEngine, serverEngine);
    355 
    356         // Trigger a renegotiation from the server and send a message back from Server->Client
    357         serverEngine.setEnabledCipherSuites(new String[] {RENEGOTIATION_CIPHER});
    358         serverEngine.beginHandshake();
    359         doHandshake(false);
    360 
    361         exchangeMessage(newMessage(MESSAGE_SIZE), serverEngine, clientEngine);
    362     }
    363 
    364     @Test
    365     public void savedSessionWorksAfterClose() throws Exception {
    366         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
    367         doHandshake(true);
    368 
    369         SSLSession session = clientEngine.getSession();
    370         String cipherSuite = session.getCipherSuite();
    371 
    372         clientEngine.closeOutbound();
    373         clientEngine.closeInbound();
    374 
    375         assertEquals(cipherSuite, session.getCipherSuite());
    376     }
    377 
    378     private void doMutualAuthHandshake(
    379             TestKeyStore clientKs, TestKeyStore serverKs, ClientAuth clientAuth) throws Exception {
    380         setupEngines(clientKs, serverKs);
    381         clientAuth.apply(serverEngine);
    382         doHandshake(true);
    383         assertEquals(HandshakeStatus.NOT_HANDSHAKING, clientEngine.getHandshakeStatus());
    384         assertEquals(HandshakeStatus.NOT_HANDSHAKING, serverEngine.getHandshakeStatus());
    385     }
    386 
    387     private void doHandshake(boolean beginHandshake) throws SSLException {
    388         ByteBuffer clientApplicationBuffer =
    389                 bufferType.newBuffer(clientEngine.getSession().getApplicationBufferSize());
    390         ByteBuffer clientPacketBuffer =
    391                 bufferType.newBuffer(clientEngine.getSession().getPacketBufferSize());
    392         ByteBuffer serverApplicationBuffer =
    393                 bufferType.newBuffer(serverEngine.getSession().getApplicationBufferSize());
    394         ByteBuffer serverPacketBuffer =
    395                 bufferType.newBuffer(serverEngine.getSession().getPacketBufferSize());
    396         TestUtils.doEngineHandshake(clientEngine, serverEngine, clientApplicationBuffer,
    397                 clientPacketBuffer, serverApplicationBuffer, serverPacketBuffer, beginHandshake);
    398     }
    399 
    400     private void setupEngines(TestKeyStore clientKeyStore, TestKeyStore serverKeyStore) throws SSLException {
    401         setupClientEngine(getConscryptProvider(), clientKeyStore);
    402         setupServerEngine(getConscryptProvider(), serverKeyStore);
    403     }
    404 
    405     private void setupClientEngine(Provider provider, TestKeyStore clientKeyStore)
    406             throws SSLException {
    407         clientEngine = newEngine(provider, clientKeyStore, true);
    408     }
    409 
    410     private void setupServerEngine(Provider provider, TestKeyStore serverKeyStore)
    411             throws SSLException {
    412         serverEngine = newEngine(provider, serverKeyStore, false);
    413     }
    414 
    415     private SSLEngine newEngine(
    416             Provider provider, TestKeyStore keyStore, boolean client) {
    417         SSLContext serverContext = newContext(provider, keyStore);
    418         SSLEngine engine = serverContext.createSSLEngine();
    419         engine.setEnabledCipherSuites(CIPHERS);
    420         engine.setUseClientMode(client);
    421         if (Conscrypt.isConscrypt(engine)) {
    422             Conscrypt.setBufferAllocator(engine, bufferType.allocator);
    423         }
    424         return engine;
    425     }
    426 
    427     private void exchangeMessage(ByteBuffer inputBuffer, SSLEngine src, SSLEngine dest)
    428             throws IOException {
    429         byte[] messageBytes = toArray(inputBuffer);
    430 
    431         // Encrypt the input message.
    432         List<ByteBuffer> encryptedBufferList = wrap(inputBuffer, src);
    433 
    434         // Unwrap the all of the encrypted messages.
    435         ByteBuffer[] encryptedBuffers =
    436                 encryptedBufferList.toArray(new ByteBuffer[encryptedBufferList.size()]);
    437         byte[] actualBytes = unwrap(encryptedBuffers, dest);
    438         assertArrayEquals(messageBytes, actualBytes);
    439     }
    440 
    441     private List<ByteBuffer> wrap(ByteBuffer input, SSLEngine engine) throws SSLException {
    442         // Encrypt the input message.
    443         List<ByteBuffer> wrapped = new ArrayList<ByteBuffer>();
    444         while (input.hasRemaining()) {
    445             ByteBuffer encryptedBuffer =
    446                     bufferType.newBuffer(engine.getSession().getPacketBufferSize());
    447             SSLEngineResult wrapResult = engine.wrap(input, encryptedBuffer);
    448             assertEquals(SSLEngineResult.Status.OK, wrapResult.getStatus());
    449             encryptedBuffer.flip();
    450             wrapped.add(encryptedBuffer);
    451         }
    452         return wrapped;
    453     }
    454 
    455     private byte[] unwrap(ByteBuffer[] encryptedBuffers, SSLEngine engine) throws IOException {
    456         ByteArrayOutputStream cleartextStream = new ByteArrayOutputStream();
    457         int decryptedBufferSize = 8192;
    458         final ByteBuffer encryptedBuffer = combine(encryptedBuffers);
    459         final ByteBuffer decryptedBuffer = bufferType.newBuffer(decryptedBufferSize);
    460         while (encryptedBuffer.hasRemaining()) {
    461             if (!decryptedBuffer.hasRemaining()) {
    462                 decryptedBuffer.clear();
    463             }
    464             int prevPos = decryptedBuffer.position();
    465             SSLEngineResult unwrapResult = engine.unwrap(encryptedBuffer, decryptedBuffer);
    466             SSLEngineResult.Status status = unwrapResult.getStatus();
    467             switch (status) {
    468                 case BUFFER_OVERFLOW:
    469                 case OK: {
    470                     break;
    471                 }
    472                 default: { throw new RuntimeException("Unexpected SSLEngine status: " + status); }
    473             }
    474             int newPos = decryptedBuffer.position();
    475             int bytesProduced = unwrapResult.bytesProduced();
    476             assertEquals(bytesProduced, newPos - prevPos);
    477 
    478             // Add any generated bytes to the output stream.
    479             if (bytesProduced > 0 || status == Status.BUFFER_OVERFLOW) {
    480                 byte[] decryptedBytes = new byte[unwrapResult.bytesProduced()];
    481 
    482                 // Read the chunk that was just written to the output array.
    483                 int limit = decryptedBuffer.limit();
    484                 decryptedBuffer.limit(newPos);
    485                 decryptedBuffer.position(prevPos);
    486                 decryptedBuffer.get(decryptedBytes);
    487 
    488                 // Restore the position and limit.
    489                 decryptedBuffer.limit(limit);
    490 
    491                 // Write the decrypted bytes to the stream.
    492                 cleartextStream.write(decryptedBytes);
    493             }
    494         }
    495 
    496         return cleartextStream.toByteArray();
    497     }
    498 
    499     private ByteBuffer combine(ByteBuffer[] buffers) {
    500         int size = 0;
    501         for (ByteBuffer buffer : buffers) {
    502             size += buffer.remaining();
    503         }
    504         ByteBuffer combined = bufferType.newBuffer(size);
    505         for (ByteBuffer buffer : buffers) {
    506             combined.put(buffer);
    507         }
    508         combined.flip();
    509         return combined;
    510     }
    511 
    512     private ByteBuffer newMessage(int size) {
    513         ByteBuffer buffer = bufferType.newBuffer(size);
    514         buffer.put(newTextMessage(size));
    515         buffer.flip();
    516         return buffer;
    517     }
    518 
    519     private static byte[] toArray(ByteBuffer buffer) {
    520         int pos = buffer.position();
    521         byte[] bytes = new byte[buffer.remaining()];
    522         buffer.get(bytes);
    523         buffer.position(pos);
    524         return bytes;
    525     }
    526 
    527     private static SSLContext newContext(Provider provider, TestKeyStore keyStore) {
    528         try {
    529             SSLContext ctx = SSLContext.getInstance(getProtocols()[0], provider);
    530             return initSslContext(ctx, keyStore);
    531         } catch (NoSuchAlgorithmException e) {
    532             throw new RuntimeException(e);
    533         }
    534     }
    535 }
    536