Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a10b9ae4 authored by cketti's avatar cketti
Browse files

Merge pull request #493 from zjw/ssl_changes

SSL changes
parents 59a61366 43c38a04
Loading
Loading
Loading
Loading
+0 −280
Original line number Diff line number Diff line
/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * 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 com.fsck.k9.helper;

import android.net.http.SslCertificate;
import android.util.Log;
import com.fsck.k9.K9;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateParsingException;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * Implements basic domain-name validation as specified by RFC2818.
 */
public class DomainNameChecker {
    private static Pattern QUICK_IP_PATTERN;
    static {
        try {
            QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$");
        } catch (PatternSyntaxException e) {
        }
    }

    private static final int ALT_DNS_NAME = 2;
    private static final int ALT_IPA_NAME = 7;

    /**
     * Checks the site certificate against the domain name of the site being
     * visited
     *
     * @param certificate
     *            The certificate to check
     * @param thisDomain
     *            The domain name of the site being visited
     * @return True iff if there is a domain match as specified by RFC2818
     */
    public static boolean match(X509Certificate certificate, String thisDomain) {
        if ((certificate == null) || (thisDomain == null)
                || thisDomain.isEmpty()) {
            return false;
        }

        thisDomain = thisDomain.toLowerCase(Locale.US);
        if (!isIpAddress(thisDomain)) {
            return matchDns(certificate, thisDomain);
        } else {
            return matchIpAddress(certificate, thisDomain);
        }
    }

    /**
     * @return True iff the domain name is specified as an IP address
     */
    private static boolean isIpAddress(String domain) {
        if ((domain == null) || domain.isEmpty()) {
            return false;
        }

        boolean rval;
        try {
            // do a quick-dirty IP match first to avoid DNS lookup
            rval = QUICK_IP_PATTERN.matcher(domain).matches();
            if (rval) {
                rval = domain.equals(InetAddress.getByName(domain)
                                     .getHostAddress());
            }
        } catch (UnknownHostException e) {
            String errorMessage = e.getMessage();
            if (errorMessage == null) {
                errorMessage = "unknown host exception";
            }

            if (K9.DEBUG) {
                Log.v(K9.LOG_TAG, "DomainNameChecker.isIpAddress(): "
                      + errorMessage);
            }

            rval = false;
        }

        return rval;
    }

    /**
     * Checks the site certificate against the IP domain name of the site being
     * visited
     *
     * @param certificate
     *            The certificate to check
     * @param thisDomain
     *            The DNS domain name of the site being visited
     * @return True iff if there is a domain match as specified by RFC2818
     */
    private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) {
        if (K9.DEBUG) {
            Log.v(K9.LOG_TAG, "DomainNameChecker.matchIpAddress(): this domain: " + thisDomain);
        }

        try {
            Collection<List<?>> subjectAltNames = certificate.getSubjectAlternativeNames();
            if (subjectAltNames != null) {
                for (List<?> altNameEntry : subjectAltNames) {
                    if ((altNameEntry != null) && (2 <= altNameEntry.size())) {
                        Integer altNameType = (Integer)(altNameEntry.get(0));
                        if (altNameType != null && altNameType.intValue() == ALT_IPA_NAME) {
                            String altName = (String)(altNameEntry.get(1));
                            if (altName != null) {
                                if (K9.DEBUG) {
                                    Log.v(K9.LOG_TAG, "alternative IP: " + altName);
                                }
                                if (thisDomain.equalsIgnoreCase(altName)) {
                                    return true;
                                }
                            }
                        }
                    }
                }
            }
        } catch (CertificateParsingException e) {
        }

        return false;
    }

    /**
     * Checks the site certificate against the DNS domain name of the site being
     * visited
     *
     * @param certificate
     *            The certificate to check
     * @param thisDomain
     *            The DNS domain name of the site being visited
     * @return True iff if there is a domain match as specified by RFC2818
     */
    private static boolean matchDns(X509Certificate certificate, String thisDomain) {
        boolean hasDns = false;
        try {
            Collection<List<?>> subjectAltNames = certificate.getSubjectAlternativeNames();
            if (subjectAltNames != null) {
                for (List<?> altNameEntry : subjectAltNames) {
                    if ((altNameEntry != null) && (2 <= altNameEntry.size())) {
                        Integer altNameType = (Integer)(altNameEntry.get(0));
                        if (altNameType != null && altNameType.intValue() == ALT_DNS_NAME) {
                            hasDns = true;
                            String altName = (String)(altNameEntry.get(1));
                            if (altName != null && matchDns(thisDomain, altName)) {
                                return true;
                            }
                        }
                    }
                }
            }
        } catch (CertificateParsingException e) {
            // one way we can get here is if an alternative name starts with
            // '*' character, which is contrary to one interpretation of the
            // spec (a valid DNS name must start with a letter); there is no
            // good way around this, and in order to be compatible we proceed
            // to check the common name (ie, ignore alternative names)
            if (K9.DEBUG) {
                String errorMessage = e.getMessage();
                if (errorMessage == null) {
                    errorMessage = "failed to parse certificate";
                }

                Log.v(K9.LOG_TAG, "DomainNameChecker.matchDns(): "
                      + errorMessage);
            }
        }

        if (!hasDns) {
            SslCertificate sslCertificate = new SslCertificate(certificate);
            return matchDns(thisDomain, sslCertificate.getIssuedTo().getCName());
        }

        return false;
    }

    /**
     * @param thisDomain
     *            The domain name of the site being visited
     * @param thatDomain
     *            The domain name from the certificate
     * @return True iff thisDomain matches thatDomain as specified by RFC2818
     */
    private static boolean matchDns(String thisDomain, String thatDomain) {
        if (K9.DEBUG) {
            Log.v(K9.LOG_TAG, "DomainNameChecker.matchDns():"
                  + " this domain: " + thisDomain + " that domain: "
                  + thatDomain);
        }

        if ((thisDomain == null) || thisDomain.isEmpty()
                || (thatDomain == null) || thatDomain.isEmpty()) {
            return false;
        }

        thatDomain = thatDomain.toLowerCase(Locale.US);

        // (a) domain name strings are equal, ignoring case: X matches X
        boolean rval = thisDomain.equals(thatDomain);
        if (!rval) {
            String[] thisDomainTokens = thisDomain.split("\\.");
            String[] thatDomainTokens = thatDomain.split("\\.");

            int thisDomainTokensNum = thisDomainTokens.length;
            int thatDomainTokensNum = thatDomainTokens.length;

            // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X
            if (thisDomainTokensNum >= thatDomainTokensNum) {
                for (int i = thatDomainTokensNum - 1; i >= 0; --i) {
                    rval = thisDomainTokens[i].equals(thatDomainTokens[i]);
                    if (!rval) {
                        // (c) OR we have a special *-match:
                        // Z.Y.X matches *.Y.X but does not match *.X
                        rval = ((i == 0) && (thisDomainTokensNum == thatDomainTokensNum));
                        if (rval) {
                            rval = thatDomainTokens[0].equals("*");
                            if (!rval) {
                                // (d) OR we have a *-component match:
                                // f*.com matches foo.com but not bar.com
                                rval = domainTokenMatch(thisDomainTokens[0],
                                                        thatDomainTokens[0]);
                            }
                        }

                        break;
                    }
                }
            }
        }

        return rval;
    }

    /**
     * @param thisDomainToken
     *            The domain token from the current domain name
     * @param thatDomainToken
     *            The domain token from the certificate
     * @return True iff thisDomainToken matches thatDomainToken, using the
     *         wildcard match as specified by RFC2818-3.1. For example, f*.com
     *         must match foo.com but not bar.com
     */
    private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) {
        if ((thisDomainToken != null) && (thatDomainToken != null)) {
            int starIndex = thatDomainToken.indexOf('*');
            if (starIndex >= 0) {
                if (thatDomainToken.length() - 1 <= thisDomainToken.length()) {
                    String prefix = thatDomainToken.substring(0, starIndex);
                    String suffix = thatDomainToken.substring(starIndex + 1);

                    return thisDomainToken.startsWith(prefix)
                           && thisDomainToken.endsWith(suffix);
                }
            }
        }

        return false;
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -96,7 +96,7 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
import com.fsck.k9.mail.store.imap.ImapUtility;
import com.fsck.k9.mail.transport.imap.ImapSettings;
import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream;

@@ -2435,7 +2435,7 @@ public class ImapStore extends Store {
                                mSettings.getPort());

                        if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
                            mSocket = SslHelper.createSslSocket(mSettings.getHost(), 
                            mSocket = TrustedSocketFactory.createSocket(mSettings.getHost(),
                                    mSettings.getPort(), mSettings.getClientCertificateAlias());
                        } else {
                            mSocket = new Socket();
@@ -2485,8 +2485,8 @@ public class ImapStore extends Store {
                        // STARTTLS
                        executeSimpleCommand("STARTTLS");

                        mSocket = SslHelper.createStartTlsSocket(mSocket, 
                                mSettings.getHost(), mSettings.getPort(), true, 
                        mSocket = TrustedSocketFactory.createSocket(mSocket,
                                mSettings.getHost(), mSettings.getPort(),
                                mSettings.getClientCertificateAlias());
                        mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
                        mIn = new PeekableInputStream(new BufferedInputStream(mSocket
+3 −3
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ import com.fsck.k9.mail.*;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;

import javax.net.ssl.SSLException;

@@ -314,7 +314,7 @@ public class Pop3Store extends Store {
            try {
                SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
                if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
                    mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
                    mSocket = TrustedSocketFactory.createSocket(mHost, mPort, mClientCertificateAlias);
                } else {
                    mSocket = new Socket();
                }
@@ -336,7 +336,7 @@ public class Pop3Store extends Store {
                    if (mCapabilities.stls) {
                        executeSimpleCommand(STLS_COMMAND);

                        mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
                        mSocket = TrustedSocketFactory.createSocket(mSocket, mHost, mPort,
                                mClientCertificateAlias);
                        mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
                        mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+3 −3
Original line number Diff line number Diff line
@@ -15,7 +15,7 @@ import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.filter.SmtpDataStuffing;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;

import javax.net.ssl.SSLException;

@@ -224,7 +224,7 @@ public class SmtpTransport extends Transport {
                try {
                    SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
                    if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
                        mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
                        mSocket = TrustedSocketFactory.createSocket(mHost, mPort, mClientCertificateAlias);
                        mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
                        secureConnection = true;
                    } else {
@@ -278,7 +278,7 @@ public class SmtpTransport extends Transport {
                if (extensions.containsKey("STARTTLS")) {
                    executeSimpleCommand("STARTTLS");

                    mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
                    mSocket = TrustedSocketFactory.createSocket(mSocket, mHost, mPort,
                            mClientCertificateAlias);

                    mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
+0 −81
Original line number Diff line number Diff line

package com.fsck.k9.net.ssl;

import java.io.IOException;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import android.util.Log;

import com.fsck.k9.K9;
import com.fsck.k9.mail.MessagingException;

/**
 * Helper class to create SSL sockets with support for client certificate
 * authentication
 */
public class SslHelper {

    private static SSLContext createSslContext(String host, int port, String clientCertificateAlias)
            throws NoSuchAlgorithmException, KeyManagementException, MessagingException {
        if (K9.DEBUG)
            Log.d(K9.LOG_TAG, "createSslContext: Client certificate alias: "
                    + clientCertificateAlias);

        KeyManager[] keyManagers;
        if (clientCertificateAlias == null  || clientCertificateAlias.isEmpty()) {
            keyManagers = null;
        } else {
            keyManagers = new KeyManager[] { new KeyChainKeyManager(K9.app, clientCertificateAlias) };
        }

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagers,
                new TrustManager[] {
                    TrustManagerFactory.get(
                            host, port)
                },
                new SecureRandom());

        return sslContext;
    }

    /**
     * Create SSL socket
     * 
     * @param host
     * @param port
     * @param clientCertificateAlias if not null, uses client certificate
     *            retrieved by this alias for authentication
     */
    public static Socket createSslSocket(String host, int port, String clientCertificateAlias)
            throws NoSuchAlgorithmException, KeyManagementException, IOException,
            MessagingException {
        SSLContext sslContext = createSslContext(host, port, clientCertificateAlias);
        return TrustedSocketFactory.createSocket(sslContext);
    }

    /**
     * Create socket for START_TLS. autoClose = true
     * 
     * @param socket
     * @param host
     * @param port
     * @param secure
     * @param clientCertificateAlias if not null, uses client certificate
     *            retrieved by this alias for authentication
     */
    public static Socket createStartTlsSocket(Socket socket, String host, int port, boolean secure,
            String clientCertificateAlias) throws NoSuchAlgorithmException,
            KeyManagementException, IOException, MessagingException {
        SSLContext sslContext = createSslContext(host, port, clientCertificateAlias);
        boolean autoClose = true;
        return TrustedSocketFactory.createSocket(sslContext, socket, host, port, autoClose);
    }
}
Loading