Handle client certificate errors

If the alias is empty or null, don't bother using KeyChainKeyManager.

If the alias is not empty, confirm that it is associated with a
certificate, otherwise throw a CertificateValidationException
which will notify the user of the problem and ask the user to
check the server settings.

Likewise, the user is notified if the client certificate was
not accepted by the server.
This commit is contained in:
Joe Steele 2014-07-25 09:13:59 -04:00
parent 231f3645f9
commit 65144e3759
4 changed files with 77 additions and 10 deletions

View File

@ -1126,4 +1126,5 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_basics_client_certificate">Use client certificate</string>
<string name="client_certificate_spinner_empty">No client certificate</string>
<string name="client_certificate_spinner_delete">Remove client certificate selection</string>
<string name="client_certificate_retrieval_failure">"Failed to retrieve client certificate for alias <xliff:g id="alias">%s</xliff:g>"</string>
</resources>

View File

@ -5,6 +5,10 @@ import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLHandshakeException;
import android.security.KeyChainException;
public class CertificateValidationException extends MessagingException {
public static final long serialVersionUID = -1;
private X509Certificate[] mCertChain;
@ -23,16 +27,57 @@ public class CertificateValidationException extends MessagingException {
private void scanForCause() {
Throwable throwable = getCause();
/* user attention is required if the certificate was deemed invalid */
/*
* User attention is required if the server certificate was deemed
* invalid or if there was a problem with a client certificate.
*
* A CertificateException is known to be thrown by the default
* X509TrustManager.checkServerTrusted() if the server certificate
* doesn't validate. The cause of the CertificateException will be a
* CertPathValidatorException. However, it's unlikely those exceptions
* will be encountered here, because they are caught in
* SecureX509TrustManager.checkServerTrusted(), which throws a
* CertificateChainException instead (an extension of
* CertificateException).
*
* A CertificateChainException will likely result in (or, be the cause
* of) an SSLHandshakeException (an extension of SSLException).
*
* The various mail protocol handlers (IMAP, POP3, ...) will catch an
* SSLException and throw a CertificateValidationException (this class)
* with the SSLException as the cause. They may also throw a
* CertificateValidationException with a new CertificateException as the
* cause when STARTTLS is not available, just for the purpose of
* triggering a user notification.
*
* SSLHandshakeException is also known to occur if the *client*
* certificate was not accepted by the server (unknown CA, certificate
* expired, etc.). In this case, the SSLHandshakeException will not have
* a CertificateChainException as a cause.
*
* KeyChainException is known to occur if the device has no client
* certificate that's associated with the alias stored in the server
* settings.
*/
while (throwable != null
&& !(throwable instanceof CertPathValidatorException)
&& !(throwable instanceof CertificateException)) {
&& !(throwable instanceof CertificateException)
&& !(throwable instanceof KeyChainException)
&& !(throwable instanceof SSLHandshakeException)) {
throwable = throwable.getCause();
}
if (throwable != null) {
mNeedsUserAttention = true;
if (throwable instanceof CertificateChainException) {
// See if there is a server certificate chain attached to the SSLHandshakeException
if (throwable instanceof SSLHandshakeException) {
while (throwable != null && !(throwable instanceof CertificateChainException)) {
throwable = throwable.getCause();
}
}
if (throwable != null && throwable instanceof CertificateChainException) {
mCertChain = ((CertificateChainException) throwable).getCertChain();
}
}

View File

@ -29,8 +29,13 @@ public class SslHelper {
Log.d(K9.LOG_TAG, "createSslContext: Client certificate alias: "
+ clientCertificateAlias);
KeyManager[] keyManagers = new KeyManager[] { new KeyChainKeyManager(
clientCertificateAlias) };
KeyManager[] keyManagers;
if (clientCertificateAlias == null || clientCertificateAlias.isEmpty()) {
keyManagers = null;
} else {
keyManagers = new KeyManager[] { new KeyChainKeyManager(
clientCertificateAlias) };
}
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers,

View File

@ -14,6 +14,9 @@ import android.security.KeyChainException;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.MessagingException;
/**
* For client certificate authentication! Provide private keys and certificates
@ -25,11 +28,24 @@ public class KeyChainKeyManager extends X509ExtendedKeyManager {
private String mAlias;
public KeyChainKeyManager(String alias) {
if (alias == null || "".equals(alias)) {
mAlias = null;
} else {
mAlias = alias;
/**
* @param alias Must not be null nor empty
* @throws MessagingException
* Indicates an error in retrieving the certificate for the alias
* (likely because the alias is invalid or the certificate was deleted)
*/
public KeyChainKeyManager(String alias) throws MessagingException {
mAlias = alias;
// Check for invalid alias (the user may have deleted the certificate)
try {
KeyChain.getCertificateChain(K9.app, alias);
} catch (KeyChainException e) {
throw new CertificateValidationException(K9.app.getString(
R.string.client_certificate_retrieval_failure, alias), e);
} catch (InterruptedException e) {
throw new MessagingException(K9.app.getString(
R.string.client_certificate_retrieval_failure, alias), e);
}
}