1/*
2 * Copyright (C) 2007 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
17package org.conscrypt;
18
19import java.io.IOException;
20import java.security.Principal;
21import java.security.cert.Certificate;
22import java.security.cert.CertificateEncodingException;
23import java.security.cert.X509Certificate;
24import java.util.HashMap;
25import java.util.Map;
26import javax.net.ssl.SSLPeerUnverifiedException;
27import javax.net.ssl.SSLSession;
28import javax.net.ssl.SSLSessionBindingEvent;
29import javax.net.ssl.SSLSessionBindingListener;
30import javax.net.ssl.SSLSessionContext;
31import javax.security.cert.CertificateException;
32
33/**
34 * Implementation of the class OpenSSLSessionImpl
35 * based on OpenSSL.
36 */
37public class OpenSSLSessionImpl implements SSLSession {
38
39    private long creationTime = 0;
40    long lastAccessedTime = 0;
41    final X509Certificate[] localCertificates;
42    final X509Certificate[] peerCertificates;
43
44    private boolean isValid = true;
45    private final Map<String, Object> values = new HashMap<String, Object>();
46    private volatile javax.security.cert.X509Certificate[] peerCertificateChain;
47    protected long sslSessionNativePointer;
48    private String peerHost;
49    private int peerPort = -1;
50    private String cipherSuite;
51    private String protocol;
52    private AbstractSessionContext sessionContext;
53    private byte[] id;
54
55    /**
56     * Class constructor creates an SSL session context given the appropriate
57     * SSL parameters.
58     */
59    protected OpenSSLSessionImpl(long sslSessionNativePointer, X509Certificate[] localCertificates,
60            X509Certificate[] peerCertificates, String peerHost, int peerPort,
61            AbstractSessionContext sessionContext) {
62        this.sslSessionNativePointer = sslSessionNativePointer;
63        this.localCertificates = localCertificates;
64        this.peerCertificates = peerCertificates;
65        this.peerHost = peerHost;
66        this.peerPort = peerPort;
67        this.sessionContext = sessionContext;
68    }
69
70    /**
71     * Constructs a session from a byte[] containing DER data. This
72     * allows loading the saved session.
73     * @throws IOException
74     */
75    OpenSSLSessionImpl(byte[] derData,
76            String peerHost, int peerPort,
77            X509Certificate[] peerCertificates,
78            AbstractSessionContext sessionContext)
79            throws IOException {
80        this(NativeCrypto.d2i_SSL_SESSION(derData),
81             null,
82             peerCertificates,
83             peerHost,
84             peerPort,
85             sessionContext);
86        // TODO move this check into native code so we can throw an error with more information
87        if (this.sslSessionNativePointer == 0) {
88            throw new IOException("Invalid session data");
89        }
90    }
91
92    /**
93     * Gets the identifier of the actual SSL session
94     * @return array of sessions' identifiers.
95     */
96    @Override
97    public byte[] getId() {
98        if (id == null) {
99            resetId();
100        }
101        return id;
102    }
103
104    /**
105     * Reset the id field to the current value found in the native
106     * SSL_SESSION. It can change during the lifetime of the session
107     * because while a session is created during initial handshake,
108     * with handshake_cutthrough, the SSL_do_handshake may return
109     * before we have read the session ticket from the server side and
110     * therefore have computed no id based on the SHA of the ticket.
111     */
112    void resetId() {
113        id = NativeCrypto.SSL_SESSION_session_id(sslSessionNativePointer);
114    }
115
116    /**
117     * Get the session object in DER format. This allows saving the session
118     * data or sharing it with other processes.
119     */
120    byte[] getEncoded() {
121        return NativeCrypto.i2d_SSL_SESSION(sslSessionNativePointer);
122    }
123
124    /**
125     * Gets the creation time of the SSL session.
126     * @return the session's creation time in milliseconds since the epoch
127     */
128    @Override
129    public long getCreationTime() {
130        if (creationTime == 0) {
131            creationTime = NativeCrypto.SSL_SESSION_get_time(sslSessionNativePointer);
132        }
133        return creationTime;
134    }
135
136    /**
137     * Returns the last time this concrete SSL session was accessed. Accessing
138     * here is to mean that a new connection with the same SSL context data was
139     * established.
140     *
141     * @return the session's last access time in milliseconds since the epoch
142     */
143    @Override
144    public long getLastAccessedTime() {
145        return (lastAccessedTime == 0) ? getCreationTime() : lastAccessedTime;
146    }
147
148    /**
149     * Returns the largest buffer size for the application's data bound to this
150     * concrete SSL session.
151     * @return the largest buffer size
152     */
153    @Override
154    public int getApplicationBufferSize() {
155        return SSLRecordProtocol.MAX_DATA_LENGTH;
156    }
157
158    /**
159     * Returns the largest SSL/TLS packet size one can expect for this concrete
160     * SSL session.
161     * @return the largest packet size
162     */
163    @Override
164    public int getPacketBufferSize() {
165        return SSLRecordProtocol.MAX_SSL_PACKET_SIZE;
166    }
167
168    /**
169     * Returns the principal (subject) of this concrete SSL session used in the
170     * handshaking phase of the connection.
171     * @return a X509 certificate or null if no principal was defined
172     */
173    @Override
174    public Principal getLocalPrincipal() {
175        if (localCertificates != null && localCertificates.length > 0) {
176            return localCertificates[0].getSubjectX500Principal();
177        } else {
178            return null;
179        }
180    }
181
182    /**
183     * Returns the certificate(s) of the principal (subject) of this concrete SSL
184     * session used in the handshaking phase of the connection. The OpenSSL
185     * native method supports only RSA certificates.
186     * @return an array of certificates (the local one first and then eventually
187     *         that of the certification authority) or null if no certificate
188     *         were used during the handshaking phase.
189     */
190    @Override
191    public Certificate[] getLocalCertificates() {
192        return localCertificates;
193    }
194
195    /**
196     * Returns the certificate(s) of the peer in this SSL session
197     * used in the handshaking phase of the connection.
198     * Please notice hat this method is superseded by
199     * <code>getPeerCertificates()</code>.
200     * @return an array of X509 certificates (the peer's one first and then
201     *         eventually that of the certification authority) or null if no
202     *         certificate were used during the SSL connection.
203     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
204     *         was used (i.e. Kerberos certificates) or the peer could not
205     *         be verified.
206     */
207    @Override
208    public javax.security.cert.X509Certificate[] getPeerCertificateChain()
209            throws SSLPeerUnverifiedException {
210        checkPeerCertificatesPresent();
211        javax.security.cert.X509Certificate[] result = peerCertificateChain;
212        if (result == null) {
213            // single-check idiom
214            peerCertificateChain = result = createPeerCertificateChain();
215        }
216        return result;
217    }
218
219    /**
220     * Provide a value to initialize the volatile peerCertificateChain
221     * field based on the native SSL_SESSION
222     */
223    private javax.security.cert.X509Certificate[] createPeerCertificateChain()
224            throws SSLPeerUnverifiedException {
225        try {
226            javax.security.cert.X509Certificate[] chain
227                    = new javax.security.cert.X509Certificate[peerCertificates.length];
228
229            for (int i = 0; i < peerCertificates.length; i++) {
230                byte[] encoded = peerCertificates[i].getEncoded();
231                chain[i] = javax.security.cert.X509Certificate.getInstance(encoded);
232            }
233            return chain;
234        } catch (CertificateEncodingException e) {
235            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
236            exception.initCause(exception);
237            throw exception;
238        } catch (CertificateException e) {
239            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
240            exception.initCause(exception);
241            throw exception;
242        }
243    }
244
245    /**
246     * Return the identity of the peer in this SSL session
247     * determined via certificate(s).
248     * @return an array of X509 certificates (the peer's one first and then
249     *         eventually that of the certification authority) or null if no
250     *         certificate were used during the SSL connection.
251     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
252     *         was used (i.e. Kerberos certificates) or the peer could not
253     *         be verified.
254     */
255    @Override
256    public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
257        checkPeerCertificatesPresent();
258        return peerCertificates;
259    }
260
261    /**
262     * Throw SSLPeerUnverifiedException on null or empty peerCertificates array
263     */
264    private void checkPeerCertificatesPresent() throws SSLPeerUnverifiedException {
265        if (peerCertificates == null || peerCertificates.length == 0) {
266            throw new SSLPeerUnverifiedException("No peer certificates");
267        }
268    }
269
270    /**
271     * The identity of the principal that was used by the peer during the SSL
272     * handshake phase is returned by this method.
273     * @return a X500Principal of the last certificate for X509-based
274     *         cipher suites.
275     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
276     *         was used (i.e. Kerberos certificates) or the peer does not exist.
277     *
278     */
279    @Override
280    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
281        checkPeerCertificatesPresent();
282        return peerCertificates[0].getSubjectX500Principal();
283    }
284
285    /**
286     * The peer's host name used in this SSL session is returned. It is the host
287     * name of the client for the server; and that of the server for the client.
288     * It is not a reliable way to get a fully qualified host name: it is mainly
289     * used internally to implement links for a temporary cache of SSL sessions.
290     *
291     * @return the host name of the peer, or {@code null} if no information is
292     *         available.
293     */
294    @Override
295    public String getPeerHost() {
296        return peerHost;
297    }
298
299    /**
300     * Returns the peer's port number for the actual SSL session. It is the port
301     * number of the client for the server; and that of the server for the
302     * client. It is not a reliable way to get a peer's port number: it is
303     * mainly used internally to implement links for a temporary cache of SSL
304     * sessions.
305     *
306     * @return the peer's port number, or {@code -1} if no one is available.
307     */
308    @Override
309    public int getPeerPort() {
310        return peerPort;
311    }
312
313    /**
314     * Returns a string identifier of the crypto tools used in the actual SSL
315     * session. For example AES_256_WITH_MD5.
316     */
317    @Override
318    public String getCipherSuite() {
319        if (cipherSuite == null) {
320            String name = NativeCrypto.SSL_SESSION_cipher(sslSessionNativePointer);
321            cipherSuite = NativeCrypto.OPENSSL_TO_STANDARD_CIPHER_SUITES.get(name);
322            if (cipherSuite == null) {
323                cipherSuite = name;
324            }
325        }
326        return cipherSuite;
327    }
328
329    /**
330     * Returns the standard version name of the SSL protocol used in all
331     * connections pertaining to this SSL session.
332     */
333    @Override
334    public String getProtocol() {
335        if (protocol == null) {
336            protocol = NativeCrypto.SSL_SESSION_get_version(sslSessionNativePointer);
337        }
338        return protocol;
339    }
340
341    /**
342     * Returns the context to which the actual SSL session is bound. A SSL
343     * context consists of (1) a possible delegate, (2) a provider and (3) a
344     * protocol.
345     * @return the SSL context used for this session, or null if it is
346     * unavailable.
347     */
348    @Override
349    public SSLSessionContext getSessionContext() {
350        return sessionContext;
351    }
352
353    /**
354     * Returns a boolean flag signaling whether a SSL session is valid
355     * and available for resuming or joining or not.
356     *
357     * @return true if this session may be resumed.
358     */
359    @Override
360    public boolean isValid() {
361        if (!isValid) {
362            return false;
363        }
364        // The session has't yet been invalidated -- check whether it timed out.
365
366        SSLSessionContext context = sessionContext;
367        if (context == null) {
368            // Session not associated with a context -- no way to tell what its timeout should be.
369            return true;
370        }
371
372        int timeoutSeconds = context.getSessionTimeout();
373        if (timeoutSeconds == 0) {
374            // Infinite timeout -- session still valid
375            return true;
376        }
377
378        long creationTimestampMillis = getCreationTime();
379        long ageSeconds = (System.currentTimeMillis() - creationTimestampMillis) / 1000;
380        // NOTE: The age might be negative if something was/is wrong with the system clock. We time
381        // out such sessions to be safe.
382        if ((ageSeconds >= timeoutSeconds) || (ageSeconds < 0)) {
383            // Session timed out -- no longer valid
384            isValid = false;
385            return false;
386        }
387
388        // Session still valid
389        return true;
390    }
391
392    /**
393     * It invalidates a SSL session forbidding any resumption.
394     */
395    @Override
396    public void invalidate() {
397        isValid = false;
398        sessionContext = null;
399    }
400
401    /**
402     * Returns the object which is bound to the the input parameter name.
403     * This name is a sort of link to the data of the SSL session's application
404     * layer, if any exists.
405     *
406     * @param name the name of the binding to find.
407     * @return the value bound to that name, or null if the binding does not
408     *         exist.
409     * @throws IllegalArgumentException if the argument is null.
410     */
411    @Override
412    public Object getValue(String name) {
413        if (name == null) {
414            throw new IllegalArgumentException("name == null");
415        }
416        return values.get(name);
417    }
418
419    /**
420     * Returns an array with the names (sort of links) of all the data
421     * objects of the application layer bound into the SSL session.
422     *
423     * @return a non-null (possibly empty) array of names of the data objects
424     *         bound to this SSL session.
425     */
426    @Override
427    public String[] getValueNames() {
428        return values.keySet().toArray(new String[values.size()]);
429    }
430
431    /**
432     * A link (name) with the specified value object of the SSL session's
433     * application layer data is created or replaced. If the new (or existing)
434     * value object implements the <code>SSLSessionBindingListener</code>
435     * interface, that object will be notified in due course.
436     *
437     * @param name the name of the link (no null are
438     *            accepted!)
439     * @param value data object that shall be bound to
440     *            name.
441     * @throws IllegalArgumentException if one or both argument(s) is null.
442     */
443    @Override
444    public void putValue(String name, Object value) {
445        if (name == null || value == null) {
446            throw new IllegalArgumentException("name == null || value == null");
447        }
448        Object old = values.put(name, value);
449        if (value instanceof SSLSessionBindingListener) {
450            ((SSLSessionBindingListener) value)
451                    .valueBound(new SSLSessionBindingEvent(this, name));
452        }
453        if (old instanceof SSLSessionBindingListener) {
454            ((SSLSessionBindingListener) old)
455                    .valueUnbound(new SSLSessionBindingEvent(this, name));
456        }
457    }
458
459    /**
460     * Removes a link (name) with the specified value object of the SSL
461     * session's application layer data.
462     *
463     * <p>If the value object implements the <code>SSLSessionBindingListener</code>
464     * interface, the object will receive a <code>valueUnbound</code> notification.
465     *
466     * @param name the name of the link (no null are
467     *            accepted!)
468     * @throws IllegalArgumentException if the argument is null.
469     */
470    @Override
471    public void removeValue(String name) {
472        if (name == null) {
473            throw new IllegalArgumentException("name == null");
474        }
475        Object old = values.remove(name);
476        if (old instanceof SSLSessionBindingListener) {
477            SSLSessionBindingListener listener = (SSLSessionBindingListener) old;
478            listener.valueUnbound(new SSLSessionBindingEvent(this, name));
479        }
480    }
481
482    @Override
483    protected void finalize() throws Throwable {
484        try {
485            NativeCrypto.SSL_SESSION_free(sslSessionNativePointer);
486        } finally {
487            super.finalize();
488        }
489    }
490}
491