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