blob: 497df1c069986ef19652932c858db5f602b5cc96 [file] [log] [blame]
/*
* Copyright 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License", "www.google.com", 443);
* 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 org.conscrypt;
import static org.conscrypt.SSLUtils.SessionType.OPEN_SSL_WITH_OCSP;
import static org.conscrypt.SSLUtils.SessionType.OPEN_SSL_WITH_TLS_SCT;
import static org.conscrypt.SSLUtils.SessionType.isSupportedType;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.List;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.security.cert.X509Certificate;
/**
* A utility wrapper that abstracts operations on the underlying native SSL_SESSION instance.
*
* This is abstract only to support mocking for tests.
*/
abstract class SslSessionWrapper {
/**
* Creates a new instance. Since BoringSSL does not provide an API to get access to all
* session information via the SSL_SESSION, we get some values (e.g. peer certs) from
* the active session instead (i.e. the SSL object).
*/
static SslSessionWrapper newInstance(NativeRef.SSL_SESSION ref, ActiveSession activeSession)
throws SSLPeerUnverifiedException {
AbstractSessionContext context = (AbstractSessionContext) activeSession.getSessionContext();
if (context instanceof ClientSessionContext) {
return new Impl(context, ref, activeSession.getPeerHost(),
activeSession.getPeerPort(), activeSession.getPeerCertificates(),
getOcspResponse(activeSession),
activeSession.getPeerSignedCertificateTimestamp());
}
// Server's will be cached by ID and won't have any of the extra fields.
return new Impl(context, ref, null, -1, null, null, null);
}
private static byte[] getOcspResponse(ActiveSession activeSession) {
List<byte[]> ocspResponseList = activeSession.getStatusResponses();
if (ocspResponseList.size() >= 1) {
return ocspResponseList.get(0);
}
return null;
}
/**
* Creates a new {@link SslSessionWrapper} instance from the provided serialized bytes, which
* were generated by {@link #toBytes()}.
*
* @return The new instance if successful. If unable to parse the bytes for any reason, returns
* {@code null}.
*/
static SslSessionWrapper newInstance(
AbstractSessionContext context, byte[] data, String host, int port) {
ByteBuffer buf = ByteBuffer.wrap(data);
try {
int type = buf.getInt();
if (!isSupportedType(type)) {
throw new IOException("Unexpected type ID: " + type);
}
int length = buf.getInt();
checkRemaining(buf, length);
byte[] sessionData = new byte[length];
buf.get(sessionData);
int count = buf.getInt();
checkRemaining(buf, count);
java.security.cert.X509Certificate[] peerCerts =
new java.security.cert.X509Certificate[count];
for (int i = 0; i < count; i++) {
length = buf.getInt();
checkRemaining(buf, length);
byte[] certData = new byte[length];
buf.get(certData);
try {
peerCerts[i] = OpenSSLX509Certificate.fromX509Der(certData);
} catch (Exception e) {
throw new IOException("Can not read certificate " + i + "/" + count);
}
}
byte[] ocspData = null;
if (type >= OPEN_SSL_WITH_OCSP.value) {
// We only support one OCSP response now, but in the future
// we may support RFC 6961 which has multiple.
int countOcspResponses = buf.getInt();
checkRemaining(buf, countOcspResponses);
if (countOcspResponses >= 1) {
int ocspLength = buf.getInt();
checkRemaining(buf, ocspLength);
ocspData = new byte[ocspLength];
buf.get(ocspData);
// Skip the rest of the responses.
for (int i = 1; i < countOcspResponses; i++) {
ocspLength = buf.getInt();
checkRemaining(buf, ocspLength);
buf.position(buf.position() + ocspLength);
}
}
}
byte[] tlsSctData = null;
if (type == OPEN_SSL_WITH_TLS_SCT.value) {
int tlsSctDataLength = buf.getInt();
checkRemaining(buf, tlsSctDataLength);
if (tlsSctDataLength > 0) {
tlsSctData = new byte[tlsSctDataLength];
buf.get(tlsSctData);
}
}
if (buf.remaining() != 0) {
log(new AssertionError("Read entire session, but data still remains; rejecting"));
return null;
}
NativeRef.SSL_SESSION ref =
new NativeRef.SSL_SESSION(NativeCrypto.d2i_SSL_SESSION(sessionData));
return new Impl(context, ref, host, port, peerCerts, ocspData, tlsSctData);
} catch (IOException e) {
log(e);
return null;
} catch (BufferUnderflowException e) {
log(e);
return null;
}
}
abstract byte[] getId();
abstract boolean isValid();
abstract void offerToResume(SslWrapper ssl) throws SSLException;
abstract String getCipherSuite();
abstract String getProtocol();
abstract String getPeerHost();
abstract int getPeerPort();
/**
* Returns the OCSP stapled response. The returned array is not copied; the caller must
* either not modify the returned array or make a copy.
*
* @see <a href="https://tools.ietf.org/html/rfc6066">RFC 6066</a>
* @see <a href="https://tools.ietf.org/html/rfc6961">RFC 6961</a>
*/
abstract byte[] getPeerOcspStapledResponse();
/**
* Returns the signed certificate timestamp (SCT) received from the peer. The returned array
* is not copied; the caller must either not modify the returned array or make a copy.
*
* @see <a href="https://tools.ietf.org/html/rfc6962">RFC 6962</a>
*/
abstract byte[] getPeerSignedCertificateTimestamp();
/**
* Converts the given session to bytes.
*
* @return session data as bytes or null if the session can't be converted
*/
abstract byte[] toBytes();
/**
* Converts this object to a {@link SSLSession}. The returned session will support only a
* subset of the {@link SSLSession} API.
*/
abstract SSLSession toSSLSession();
/**
* The session wrapper implementation.
*/
private static final class Impl extends SslSessionWrapper {
private final NativeRef.SSL_SESSION ref;
// BoringSSL offers no API to obtain these values directly from the SSL_SESSION.
private final AbstractSessionContext context;
private final String host;
private final int port;
private final String protocol;
private final String cipherSuite;
private final java.security.cert.X509Certificate[] peerCertificates;
private final byte[] peerOcspStapledResponse;
private final byte[] peerSignedCertificateTimestamp;
private Impl(AbstractSessionContext context, NativeRef.SSL_SESSION ref, String host,
int port, java.security.cert.X509Certificate[] peerCertificates,
byte[] peerOcspStapledResponse, byte[] peerSignedCertificateTimestamp) {
this.context = context;
this.host = host;
this.port = port;
this.peerCertificates = peerCertificates;
this.peerOcspStapledResponse = peerOcspStapledResponse;
this.peerSignedCertificateTimestamp = peerSignedCertificateTimestamp;
this.protocol = NativeCrypto.SSL_SESSION_get_version(ref.context);
this.cipherSuite = SSLUtils.getCipherSuiteFromName(
NativeCrypto.SSL_SESSION_cipher(ref.context));
this.ref = ref;
}
@Override
byte[] getId() {
return NativeCrypto.SSL_SESSION_session_id(ref.context);
}
private long getCreationTime() {
return NativeCrypto.SSL_SESSION_get_time(ref.context);
}
@Override
boolean isValid() {
long creationTimeMillis = getCreationTime();
// Use the minimum of the timeout from the context and the session.
long timeoutMillis =
Math.max(0,
Math.min(context.getSessionTimeout(),
NativeCrypto.SSL_SESSION_get_timeout(ref.context)))
* 1000;
return (System.currentTimeMillis() - timeoutMillis) < creationTimeMillis;
}
@Override
void offerToResume(SslWrapper ssl) throws SSLException {
ssl.offerToResumeSession(ref.context);
}
@Override
String getCipherSuite() {
return cipherSuite;
}
@Override
String getProtocol() {
return protocol;
}
@Override
String getPeerHost() {
return host;
}
@Override
int getPeerPort() {
return port;
}
@Override
byte[] getPeerOcspStapledResponse() {
return peerOcspStapledResponse;
}
@Override
byte[] getPeerSignedCertificateTimestamp() {
return peerSignedCertificateTimestamp;
}
@Override
byte[] toBytes() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream daos = new DataOutputStream(baos);
daos.writeInt(OPEN_SSL_WITH_TLS_SCT.value); // session type ID
// Session data.
byte[] data = NativeCrypto.i2d_SSL_SESSION(ref.context);
daos.writeInt(data.length);
daos.write(data);
// Certificates.
daos.writeInt(peerCertificates.length);
for (Certificate cert : peerCertificates) {
data = cert.getEncoded();
daos.writeInt(data.length);
daos.write(data);
}
if (peerOcspStapledResponse != null) {
daos.writeInt(1);
daos.writeInt(peerOcspStapledResponse.length);
daos.write(peerOcspStapledResponse);
} else {
daos.writeInt(0);
}
if (peerSignedCertificateTimestamp != null) {
daos.writeInt(peerSignedCertificateTimestamp.length);
daos.write(peerSignedCertificateTimestamp);
} else {
daos.writeInt(0);
}
// TODO: local certificates?
return baos.toByteArray();
} catch (IOException e) {
// TODO(nathanmittler): Better error handling?
System.err.println("Failed to convert saved SSL Session: " + e.getMessage());
return null;
} catch (CertificateEncodingException e) {
log(e);
return null;
}
}
@Override
SSLSession toSSLSession() {
return new SSLSession() {
@Override
public byte[] getId() {
return Impl.this.getId();
}
@Override
public String getCipherSuite() {
return Impl.this.getCipherSuite();
}
@Override
public String getProtocol() {
return Impl.this.getProtocol();
}
@Override
public String getPeerHost() {
return Impl.this.getPeerHost();
}
@Override
public int getPeerPort() {
return Impl.this.getPeerPort();
}
@Override
public long getCreationTime() {
return Impl.this.getCreationTime();
}
@Override
public boolean isValid() {
return Impl.this.isValid();
}
// UNSUPPORTED OPERATIONS
@Override
public SSLSessionContext getSessionContext() {
throw new UnsupportedOperationException();
}
@Override
public long getLastAccessedTime() {
throw new UnsupportedOperationException();
}
@Override
public void invalidate() {
throw new UnsupportedOperationException();
}
@Override
public void putValue(String s, Object o) {
throw new UnsupportedOperationException();
}
@Override
public Object getValue(String s) {
throw new UnsupportedOperationException();
}
@Override
public void removeValue(String s) {
throw new UnsupportedOperationException();
}
@Override
public String[] getValueNames() {
throw new UnsupportedOperationException();
}
@Override
public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
throw new UnsupportedOperationException();
}
@Override
public Certificate[] getLocalCertificates() {
throw new UnsupportedOperationException();
}
@Override
public X509Certificate[] getPeerCertificateChain()
throws SSLPeerUnverifiedException {
throw new UnsupportedOperationException();
}
@Override
public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
throw new UnsupportedOperationException();
}
@Override
public Principal getLocalPrincipal() {
throw new UnsupportedOperationException();
}
@Override
public int getPacketBufferSize() {
throw new UnsupportedOperationException();
}
@Override
public int getApplicationBufferSize() {
throw new UnsupportedOperationException();
}
};
}
}
private static void log(Throwable t) {
// TODO(nathanmittler): Better error handling?
System.out.println("Error inflating SSL session: "
+ (t.getMessage() != null ? t.getMessage() : t.getClass().getName()));
}
private static void checkRemaining(ByteBuffer buf, int length) throws IOException {
if (length < 0) {
throw new IOException("Length is negative: " + length);
}
if (length > buf.remaining()) {
throw new IOException(
"Length of blob is longer than available: " + length + " > " + buf.remaining());
}
}
}