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