Home | History | Annotate | Download | only in conscrypt
      1 /*
      2  * Copyright 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License", "www.google.com", 443);
      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.SSLUtils.SessionType.OPEN_SSL_WITH_OCSP;
     20 import static org.conscrypt.SSLUtils.SessionType.OPEN_SSL_WITH_TLS_SCT;
     21 import static org.conscrypt.SSLUtils.SessionType.isSupportedType;
     22 
     23 import java.io.ByteArrayOutputStream;
     24 import java.io.DataOutputStream;
     25 import java.io.IOException;
     26 import java.nio.BufferUnderflowException;
     27 import java.nio.ByteBuffer;
     28 import java.security.Principal;
     29 import java.security.cert.Certificate;
     30 import java.security.cert.CertificateEncodingException;
     31 import java.util.List;
     32 import java.util.logging.Level;
     33 import java.util.logging.Logger;
     34 import javax.net.ssl.SSLException;
     35 import javax.net.ssl.SSLPeerUnverifiedException;
     36 import javax.net.ssl.SSLSession;
     37 import javax.net.ssl.SSLSessionContext;
     38 import javax.security.cert.X509Certificate;
     39 
     40 /**
     41  * A utility wrapper that abstracts operations on the underlying native SSL_SESSION instance.
     42  *
     43  * This is abstract only to support mocking for tests.
     44  */
     45 abstract class NativeSslSession {
     46     private static final Logger logger = Logger.getLogger(NativeSslSession.class.getName());
     47 
     48     /**
     49      * Creates a new instance. Since BoringSSL does not provide an API to get access to all
     50      * session information via the SSL_SESSION, we get some values (e.g. peer certs) from
     51      * the {@link ConscryptSession} instead (i.e. the SSL object).
     52      */
     53     static NativeSslSession newInstance(NativeRef.SSL_SESSION ref, ConscryptSession session)
     54             throws SSLPeerUnverifiedException {
     55         AbstractSessionContext context = (AbstractSessionContext) session.getSessionContext();
     56         if (context instanceof ClientSessionContext) {
     57             return new Impl(context, ref, session.getPeerHost(), session.getPeerPort(),
     58                 session.getPeerCertificates(), getOcspResponse(session),
     59                 session.getPeerSignedCertificateTimestamp());
     60         }
     61 
     62         // Server's will be cached by ID and won't have any of the extra fields.
     63         return new Impl(context, ref, null, -1, null, null, null);
     64     }
     65 
     66     private static byte[] getOcspResponse(ConscryptSession session) {
     67         List<byte[]> ocspResponseList = session.getStatusResponses();
     68         if (ocspResponseList.size() >= 1) {
     69             return ocspResponseList.get(0);
     70         }
     71         return null;
     72     }
     73 
     74     /**
     75      * Creates a new {@link NativeSslSession} instance from the provided serialized bytes, which
     76      * were generated by {@link #toBytes()}.
     77      *
     78      * @return The new instance if successful. If unable to parse the bytes for any reason, returns
     79      * {@code null}.
     80      */
     81     static NativeSslSession newInstance(
     82             AbstractSessionContext context, byte[] data, String host, int port) {
     83         ByteBuffer buf = ByteBuffer.wrap(data);
     84         try {
     85             int type = buf.getInt();
     86             if (!isSupportedType(type)) {
     87                 throw new IOException("Unexpected type ID: " + type);
     88             }
     89 
     90             int length = buf.getInt();
     91             checkRemaining(buf, length);
     92 
     93             byte[] sessionData = new byte[length];
     94             buf.get(sessionData);
     95 
     96             int count = buf.getInt();
     97             checkRemaining(buf, count);
     98 
     99             java.security.cert.X509Certificate[] peerCerts =
    100                     new java.security.cert.X509Certificate[count];
    101             for (int i = 0; i < count; i++) {
    102                 length = buf.getInt();
    103                 checkRemaining(buf, length);
    104 
    105                 byte[] certData = new byte[length];
    106                 buf.get(certData);
    107                 try {
    108                     peerCerts[i] = OpenSSLX509Certificate.fromX509Der(certData);
    109                 } catch (Exception e) {
    110                     throw new IOException("Can not read certificate " + i + "/" + count);
    111                 }
    112             }
    113 
    114             byte[] ocspData = null;
    115             if (type >= OPEN_SSL_WITH_OCSP.value) {
    116                 // We only support one OCSP response now, but in the future
    117                 // we may support RFC 6961 which has multiple.
    118                 int countOcspResponses = buf.getInt();
    119                 checkRemaining(buf, countOcspResponses);
    120 
    121                 if (countOcspResponses >= 1) {
    122                     int ocspLength = buf.getInt();
    123                     checkRemaining(buf, ocspLength);
    124 
    125                     ocspData = new byte[ocspLength];
    126                     buf.get(ocspData);
    127 
    128                     // Skip the rest of the responses.
    129                     for (int i = 1; i < countOcspResponses; i++) {
    130                         ocspLength = buf.getInt();
    131                         checkRemaining(buf, ocspLength);
    132                         buf.position(buf.position() + ocspLength);
    133                     }
    134                 }
    135             }
    136 
    137             byte[] tlsSctData = null;
    138             if (type == OPEN_SSL_WITH_TLS_SCT.value) {
    139                 int tlsSctDataLength = buf.getInt();
    140                 checkRemaining(buf, tlsSctDataLength);
    141 
    142                 if (tlsSctDataLength > 0) {
    143                     tlsSctData = new byte[tlsSctDataLength];
    144                     buf.get(tlsSctData);
    145                 }
    146             }
    147 
    148             if (buf.remaining() != 0) {
    149                 log(new AssertionError("Read entire session, but data still remains; rejecting"));
    150                 return null;
    151             }
    152 
    153             NativeRef.SSL_SESSION ref =
    154                     new NativeRef.SSL_SESSION(NativeCrypto.d2i_SSL_SESSION(sessionData));
    155             return new Impl(context, ref, host, port, peerCerts, ocspData, tlsSctData);
    156         } catch (IOException e) {
    157             log(e);
    158             return null;
    159         } catch (BufferUnderflowException e) {
    160             log(e);
    161             return null;
    162         }
    163     }
    164 
    165     abstract byte[] getId();
    166 
    167     abstract boolean isValid();
    168 
    169     abstract void offerToResume(NativeSsl ssl) throws SSLException;
    170 
    171     abstract String getCipherSuite();
    172 
    173     abstract String getProtocol();
    174 
    175     abstract String getPeerHost();
    176 
    177     abstract int getPeerPort();
    178 
    179     /**
    180      * Returns the OCSP stapled response. The returned array is not copied; the caller must
    181      * either not modify the returned array or make a copy.
    182      *
    183      * @see <a href="https://tools.ietf.org/html/rfc6066">RFC 6066</a>
    184      * @see <a href="https://tools.ietf.org/html/rfc6961">RFC 6961</a>
    185      */
    186     abstract byte[] getPeerOcspStapledResponse();
    187 
    188     /**
    189      * Returns the signed certificate timestamp (SCT) received from the peer. The returned array
    190      * is not copied; the caller must either not modify the returned array or make a copy.
    191      *
    192      * @see <a href="https://tools.ietf.org/html/rfc6962">RFC 6962</a>
    193      */
    194     abstract byte[] getPeerSignedCertificateTimestamp();
    195 
    196     /**
    197      * Converts the given session to bytes.
    198      *
    199      * @return session data as bytes or null if the session can't be converted
    200      */
    201     abstract byte[] toBytes();
    202 
    203     /**
    204      * Converts this object to a {@link SSLSession}. The returned session will support only a
    205      * subset of the {@link SSLSession} API.
    206      */
    207     abstract SSLSession toSSLSession();
    208 
    209     /**
    210      * The session wrapper implementation.
    211      */
    212     private static final class Impl extends NativeSslSession {
    213         private final NativeRef.SSL_SESSION ref;
    214 
    215         // BoringSSL offers no API to obtain these values directly from the SSL_SESSION.
    216         private final AbstractSessionContext context;
    217         private final String host;
    218         private final int port;
    219         private final String protocol;
    220         private final String cipherSuite;
    221         private final java.security.cert.X509Certificate[] peerCertificates;
    222         private final byte[] peerOcspStapledResponse;
    223         private final byte[] peerSignedCertificateTimestamp;
    224 
    225         private Impl(AbstractSessionContext context, NativeRef.SSL_SESSION ref, String host,
    226                 int port, java.security.cert.X509Certificate[] peerCertificates,
    227                 byte[] peerOcspStapledResponse, byte[] peerSignedCertificateTimestamp) {
    228             this.context = context;
    229             this.host = host;
    230             this.port = port;
    231             this.peerCertificates = peerCertificates;
    232             this.peerOcspStapledResponse = peerOcspStapledResponse;
    233             this.peerSignedCertificateTimestamp = peerSignedCertificateTimestamp;
    234             this.protocol = NativeCrypto.SSL_SESSION_get_version(ref.context);
    235             this.cipherSuite =
    236                     NativeCrypto.cipherSuiteToJava(NativeCrypto.SSL_SESSION_cipher(ref.context));
    237             this.ref = ref;
    238         }
    239 
    240         @Override
    241         byte[] getId() {
    242             return NativeCrypto.SSL_SESSION_session_id(ref.context);
    243         }
    244 
    245         private long getCreationTime() {
    246             return NativeCrypto.SSL_SESSION_get_time(ref.context);
    247         }
    248 
    249         @Override
    250         boolean isValid() {
    251             long creationTimeMillis = getCreationTime();
    252             // Use the minimum of the timeout from the context and the session.
    253             long timeoutMillis = Math.max(0,
    254                                          Math.min(context.getSessionTimeout(),
    255                                                  NativeCrypto.SSL_SESSION_get_timeout(ref.context)))
    256                     * 1000;
    257             return (System.currentTimeMillis() - timeoutMillis) < creationTimeMillis;
    258         }
    259 
    260         @Override
    261         void offerToResume(NativeSsl ssl) throws SSLException {
    262             ssl.offerToResumeSession(ref.context);
    263         }
    264 
    265         @Override
    266         String getCipherSuite() {
    267             return cipherSuite;
    268         }
    269 
    270         @Override
    271         String getProtocol() {
    272             return protocol;
    273         }
    274 
    275         @Override
    276         String getPeerHost() {
    277             return host;
    278         }
    279 
    280         @Override
    281         int getPeerPort() {
    282             return port;
    283         }
    284 
    285         @Override
    286         byte[] getPeerOcspStapledResponse() {
    287             return peerOcspStapledResponse;
    288         }
    289 
    290         @Override
    291         byte[] getPeerSignedCertificateTimestamp() {
    292             return peerSignedCertificateTimestamp;
    293         }
    294 
    295         @Override
    296         byte[] toBytes() {
    297             try {
    298                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
    299                 DataOutputStream daos = new DataOutputStream(baos);
    300 
    301                 daos.writeInt(OPEN_SSL_WITH_TLS_SCT.value); // session type ID
    302 
    303                 // Session data.
    304                 byte[] data = NativeCrypto.i2d_SSL_SESSION(ref.context);
    305                 daos.writeInt(data.length);
    306                 daos.write(data);
    307 
    308                 // Certificates.
    309                 daos.writeInt(peerCertificates.length);
    310 
    311                 for (Certificate cert : peerCertificates) {
    312                     data = cert.getEncoded();
    313                     daos.writeInt(data.length);
    314                     daos.write(data);
    315                 }
    316 
    317                 if (peerOcspStapledResponse != null) {
    318                     daos.writeInt(1);
    319                     daos.writeInt(peerOcspStapledResponse.length);
    320                     daos.write(peerOcspStapledResponse);
    321                 } else {
    322                     daos.writeInt(0);
    323                 }
    324 
    325                 if (peerSignedCertificateTimestamp != null) {
    326                     daos.writeInt(peerSignedCertificateTimestamp.length);
    327                     daos.write(peerSignedCertificateTimestamp);
    328                 } else {
    329                     daos.writeInt(0);
    330                 }
    331 
    332                 // TODO: local certificates?
    333 
    334                 return baos.toByteArray();
    335             } catch (IOException e) {
    336                 // TODO(nathanmittler): Better error handling?
    337                 logger.log(Level.WARNING, "Failed to convert saved SSL Session: ", e);
    338                 return null;
    339             } catch (CertificateEncodingException e) {
    340                 log(e);
    341                 return null;
    342             }
    343         }
    344 
    345         @Override
    346         SSLSession toSSLSession() {
    347             return new SSLSession() {
    348                 @Override
    349                 public byte[] getId() {
    350                     return Impl.this.getId();
    351                 }
    352 
    353                 @Override
    354                 public String getCipherSuite() {
    355                     return Impl.this.getCipherSuite();
    356                 }
    357 
    358                 @Override
    359                 public String getProtocol() {
    360                     return Impl.this.getProtocol();
    361                 }
    362 
    363                 @Override
    364                 public String getPeerHost() {
    365                     return Impl.this.getPeerHost();
    366                 }
    367 
    368                 @Override
    369                 public int getPeerPort() {
    370                     return Impl.this.getPeerPort();
    371                 }
    372 
    373                 @Override
    374                 public long getCreationTime() {
    375                     return Impl.this.getCreationTime();
    376                 }
    377 
    378                 @Override
    379                 public boolean isValid() {
    380                     return Impl.this.isValid();
    381                 }
    382 
    383                 // UNSUPPORTED OPERATIONS
    384 
    385                 @Override
    386                 public SSLSessionContext getSessionContext() {
    387                     throw new UnsupportedOperationException();
    388                 }
    389 
    390                 @Override
    391                 public long getLastAccessedTime() {
    392                     throw new UnsupportedOperationException();
    393                 }
    394 
    395                 @Override
    396                 public void invalidate() {
    397                     throw new UnsupportedOperationException();
    398                 }
    399 
    400                 @Override
    401                 public void putValue(String s, Object o) {
    402                     throw new UnsupportedOperationException();
    403                 }
    404 
    405                 @Override
    406                 public Object getValue(String s) {
    407                     throw new UnsupportedOperationException();
    408                 }
    409 
    410                 @Override
    411                 public void removeValue(String s) {
    412                     throw new UnsupportedOperationException();
    413                 }
    414 
    415                 @Override
    416                 public String[] getValueNames() {
    417                     throw new UnsupportedOperationException();
    418                 }
    419 
    420                 @Override
    421                 public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
    422                     throw new UnsupportedOperationException();
    423                 }
    424 
    425                 @Override
    426                 public Certificate[] getLocalCertificates() {
    427                     throw new UnsupportedOperationException();
    428                 }
    429 
    430                 @Override
    431                 public X509Certificate[] getPeerCertificateChain()
    432                         throws SSLPeerUnverifiedException {
    433                     throw new UnsupportedOperationException();
    434                 }
    435 
    436                 @Override
    437                 public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
    438                     throw new UnsupportedOperationException();
    439                 }
    440 
    441                 @Override
    442                 public Principal getLocalPrincipal() {
    443                     throw new UnsupportedOperationException();
    444                 }
    445 
    446                 @Override
    447                 public int getPacketBufferSize() {
    448                     throw new UnsupportedOperationException();
    449                 }
    450 
    451                 @Override
    452                 public int getApplicationBufferSize() {
    453                     throw new UnsupportedOperationException();
    454                 }
    455             };
    456         }
    457     }
    458 
    459     private static void log(Throwable t) {
    460         // TODO(nathanmittler): Better error handling?
    461         logger.log(Level.INFO, "Error inflating SSL session: {0}",
    462                 (t.getMessage() != null ? t.getMessage() : t.getClass().getName()));
    463     }
    464 
    465     private static void checkRemaining(ByteBuffer buf, int length) throws IOException {
    466         if (length < 0) {
    467             throw new IOException("Length is negative: " + length);
    468         }
    469         if (length > buf.remaining()) {
    470             throw new IOException(
    471                     "Length of blob is longer than available: " + length + " > " + buf.remaining());
    472         }
    473     }
    474 }
    475