Commit c342cbd8 authored by Ricki Hirner's avatar Ricki Hirner

Initial commit

parents
build/
cert4android.iml
apply plugin: 'com.android.library'
android {
compileSdkVersion 24
buildToolsVersion "24.0.1"
defaultConfig {
minSdkVersion 9
targetSdkVersion 24
}
}
dependencies {
compile 'com.android.support:appcompat-v7:24.+'
compile 'com.android.support:cardview-v7:24.+'
}
\ No newline at end of file
android.library=true
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.cert4android">
<application>
<service android:name=".CustomCertService"/>
<activity
android:name=".TrustCertificateActivity"
android:label="@string/certificate_notification_connection_security"
android:launchMode="singleInstance"
android:excludeFromRecents="true"/>
</application>
</manifest>
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
public class CertUtils {
@Nullable
public static X509TrustManager getTrustManager(@Nullable KeyStore keyStore) {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(keyStore);
for (TrustManager trustManager : tmf.getTrustManagers())
if (trustManager instanceof X509TrustManager)
return (X509TrustManager)trustManager;
} catch(NoSuchAlgorithmException|KeyStoreException e) {
Constants.log.log(Level.SEVERE, "Couldn't initialize trust manager", e);
}
return null;
}
@NonNull
public static String getTag(@NonNull X509Certificate cert) {
StringBuilder sb = new StringBuilder();
for (byte b: cert.getSignature())
sb.append(Integer.toHexString(b & 0xFF));
return sb.toString();
}
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android;
import java.util.logging.Logger;
public class Constants {
public static Logger log = Logger.getLogger("cert4android");
public static int NOTIFICATION_CERT_DECISION = 88809;
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import java.io.Closeable;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509TrustManager;
/**
* TrustManager to handle custom certificates. Communicates with
* {@link CustomCertService} to fetch information about custom certificate
* trustworthiness. The IPC with a service is required when multiple processes,
* each of them with an own {@link CustomCertManager}, want to access a synchronized central
* certificate trust store + UI (for accepting certificates etc.).
*
* @author Ricki Hirner
*/
public class CustomCertManager implements X509TrustManager, Closeable {
/** how log to wait for a decision from {@link CustomCertService} */
protected static final int SERVICE_TIMEOUT = 5*60*1000;
final Context context;
/** for sending requests to {@link CustomCertService} */
Messenger service;
/** to receive replies from {@link CustomCertService} */
final Messenger messenger;
final AtomicInteger nextDecisionID = new AtomicInteger();
final SparseArray<Boolean> decisions = new SparseArray<>();
final Object decisionLock = new Object();
/** system-default trust store */
final X509TrustManager systemTrustManager;
/** Whether to launch {@link TrustCertificateActivity} directly. The notification will always be shown. */
public boolean appInForeground = true;
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
Constants.log.fine("Connected to service");
service = new Messenger(binder);
}
@Override
public void onServiceDisconnected(ComponentName className) {
service = null;
}
};
/**
* Creates a new instance.
* @param context used to bind to {@link CustomCertService}
* @param trustSystemCerts whether to trust system/user-installed CAs (default trust store)
*/
public CustomCertManager(@NonNull Context context, boolean trustSystemCerts) {
this.context = context;
systemTrustManager = trustSystemCerts ? CertUtils.getTrustManager(null) : null;
HandlerThread thread = new HandlerThread("CustomCertificateManagerMessenger");
thread.start();
messenger = new Messenger(new Handler(thread.getLooper(), new MessageHandler()));
if (!context.bindService(new Intent(context, CustomCertService.class), serviceConnection, Context.BIND_AUTO_CREATE))
throw new IllegalArgumentException("Couldn't bind service to this context");
}
@Override
public void close() {
if (serviceConnection != null)
context.unbindService(serviceConnection);
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throw new CertificateException("cert4android doesn't validate client certificates");
}
/**
* Checks whether a certificate is trusted. If {@link #systemTrustManager} is null (because
* system certificates are not being trusted or available), the first certificate in the chain
* (which is the lowest one, i.e. the actual server certificate) is passed to
* {@link CustomCertService} for further decision.
* @param chain certificate chain to check
* @param authType authentication type (ignored)
* @throws CertificateException in case of an untrusted or questionable certificate
*/
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
boolean trusted = false;
if (systemTrustManager != null)
try {
systemTrustManager.checkServerTrusted(chain, authType);
trusted = true;
} catch(CertificateException e) {
Constants.log.fine("Certificate not trusted by system");
}
if (!trusted)
checkCustomTrusted(chain[0]);
}
protected void checkCustomTrusted(X509Certificate cert) throws CertificateException {
Constants.log.fine("Querying custom certificate trustworthiness");
Message msg = Message.obtain();
msg.what = CustomCertService.MSG_CHECK_TRUSTED;
int id = msg.arg1 = nextDecisionID.getAndIncrement();
msg.replyTo = messenger;
Bundle data = new Bundle();
data.putSerializable(CustomCertService.MSG_DATA_CERTIFICATE, cert);
data.putBoolean(CustomCertService.MSG_DATA_APP_IN_FOREGROUND, appInForeground);
msg.setData(data);
try {
service.send(msg);
} catch(RemoteException ex) {
throw new CertificateException("Couldn't query custom certificate trustworthiness", ex);
}
// wait for a reply
long startTime = System.currentTimeMillis();
synchronized(decisionLock) {
while (System.currentTimeMillis() < startTime + SERVICE_TIMEOUT) {
try {
decisionLock.wait(SERVICE_TIMEOUT);
} catch(InterruptedException ex) {
throw new CertificateException("Trustworthiness check interrupted", ex);
}
Boolean decision = decisions.get(id);
if (decision != null) {
decisions.delete(id);
if (decision)
// certificate trusted
return;
else
throw new CertificateException("Certificate not trusted");
}
}
// timeout occurred
throw new CertificateException("Timeout when waiting for certificate trustworthiness decision");
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
// custom methods
public HostnameVerifier hostnameVerifier(@Nullable HostnameVerifier defaultVerifier) {
return new CustomHostnameVerifier(defaultVerifier);
}
public void resetCertificates() {
Intent intent = new Intent(context, CustomCertService.class);
intent.setAction(CustomCertService.CMD_RESET_CERTIFICATES);
context.startService(intent);
}
// Messenger for receiving replies from CustomCertificateService
public static final int MSG_CERTIFICATE_DECISION = 0;
// arg1: id
// arg2: 1: trusted, 0: not trusted
private class MessageHandler implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
Constants.log.fine("Received reply from CustomCertificateService: " + msg);
switch (msg.what) {
case MSG_CERTIFICATE_DECISION:
synchronized(decisionLock) {
decisions.put(msg.arg1, msg.arg2 != 0);
decisionLock.notifyAll();
}
return true;
}
return false;
}
}
// hostname verifier
protected class CustomHostnameVerifier implements HostnameVerifier {
final HostnameVerifier defaultVerifier;
public CustomHostnameVerifier(HostnameVerifier defaultVerifier) {
this.defaultVerifier = defaultVerifier;
}
@Override
public boolean verify(String host, SSLSession sslSession) {
Constants.log.fine("Verifying certificate for " + host);
if (defaultVerifier != null && defaultVerifier.verify(host, sslSession))
return true;
// default hostname verifier couldn't verify the hostname →
// accept the hostname as verified only if the certificate has been accepted be the user
try {
Certificate[] cert = sslSession.getPeerCertificates();
if (cert instanceof X509Certificate[] && cert.length > 0) {
checkCustomTrusted((X509Certificate)cert[0]);
Constants.log.fine("Certificate is in custom trust store, accepting");
return true;
}
} catch(SSLPeerUnverifiedException e) {
Constants.log.log(Level.WARNING, "Couldn't get certificate for host name verification", e);
} catch (CertificateException ignored) {
}
return false;
}
}
}
This diff is collapsed.
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.LinkedList;
import java.util.List;
public class TrustCertificateActivity extends AppCompatActivity {
public static final String EXTRA_CERTIFICATE = "certificate";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_trust_certificate);
showCertificate();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
showCertificate();
}
protected void showCertificate() {
X509Certificate cert = (X509Certificate)getIntent().getSerializableExtra(EXTRA_CERTIFICATE);
final String subject;
try {
if (cert.getIssuerAlternativeNames() != null) {
StringBuilder sb = new StringBuilder();
for (List altName : cert.getSubjectAlternativeNames()) {
Object name = altName.get(1);
if (name instanceof String)
sb .append("[").append(altName.get(0)).append("]")
.append(name).append(" ");
}
subject = sb.toString();
} else
subject = cert.getSubjectDN().getName();
TextView tv = (TextView)findViewById(R.id.issuedFor);
tv.setText(subject);
tv = (TextView)findViewById(R.id.issuedBy);
tv.setText(cert.getIssuerDN().toString());
DateFormat formatter = DateFormat.getDateInstance(DateFormat.LONG);
tv = (TextView)findViewById(R.id.validity_period);
tv.setText(getString(R.string.trust_certificate_validity_period_value,
formatter.format(cert.getNotBefore()),
formatter.format(cert.getNotAfter())));
tv = (TextView)findViewById(R.id.fingerprint_sha1);
tv.setText(fingerprint(cert, "SHA-1"));
tv = (TextView)findViewById(R.id.fingerprint_sha256);
tv.setText(fingerprint(cert, "SHA-256"));
} catch(CertificateParsingException e) {
e.printStackTrace();
}
final Button btnAccept = (Button)findViewById(R.id.accept);
CheckBox cb = (CheckBox)findViewById(R.id.fingerprint_ok);
cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean state) {
btnAccept.setEnabled(state);
}
});
}
@Override
public void onBackPressed() {
rejectCertificate(null);
}
public void acceptCertificate(View view) {
sendDecision(true);
finish();
}
public void rejectCertificate(View view) {
sendDecision(false);
finish();
}
protected void sendDecision(boolean trusted) {
Intent intent = new Intent(this, CustomCertService.class);
intent.setAction(CustomCertService.CMD_CERTIFICATION_DECISION);
intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, getIntent().getSerializableExtra(EXTRA_CERTIFICATE));
intent.putExtra(CustomCertService.EXTRA_TRUSTED, trusted);
startService(intent);
}
private static String fingerprint(X509Certificate cert, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
return algorithm + ": " + hexString(md.digest(cert.getEncoded()));
} catch(NoSuchAlgorithmException|CertificateEncodingException e) {
return e.getMessage();
}
}
private static String hexString(byte[] data) {
List<String> sb = new LinkedList<>();
for (byte b : data)
sb.add(Integer.toHexString(b & 0xFF));
return TextUtils.join(":", sb);
}
}
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_margin="@dimen/activity_margin">
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/trust_certificate_unknown_certificate_found"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextView.Heading"
android:layout_marginBottom="16dp"
android:text="@string/trust_certificate_x509_certificate_details"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/trust_certificate_issued_for"/>
<TextView
android:id="@+id/issuedFor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
tools:text="CN=example.com"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/trust_certificate_issued_by"/>
<TextView
android:id="@+id/issuedBy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
tools:text="CN=example.com"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/trust_certificate_validity_period"/>
<TextView
android:id="@+id/validity_period"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
tools:text="1.1.1000 – 2.2.2000"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/trust_certificate_fingerprints"/>
<TextView
android:id="@+id/fingerprint_sha1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="SHA-1: abcdef"/>
<TextView
android:id="@+id/fingerprint_sha256"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
tools:text="SHA-256: abcdef"/>
<CheckBox
android:id="@+id/fingerprint_ok"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_marginBottom="8dp"
android:text="@string/trust_certificate_fingerprint_verified"/>
<android.support.v7.widget.ButtonBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:text="@string/trust_certificate_accept"
android:onClick="acceptCertificate"
android:enabled="false"/>
<Button
android:id="@+id/reject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.Borderless"
android:text="@string/trust_certificate_reject"
android:onClick="rejectCertificate"/>
</android.support.v7.widget.ButtonBarLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/trust_certificate_reset_info"/>
</LinearLayout>
</ScrollView>
\ No newline at end of file
<
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="certificate_notification_connection_security">Verbindungssicherheit</string>
<string name="certificate_notification_user_interaction">Bitte Zertifikat überprüfen</string>
<string name="trust_certificate_unknown_certificate_found">cert4android ist auf ein unbekanntes Zertifikat gestoßen. Wollen Sie diesem vertrauen?</string>
<string name="trust_certificate_x509_certificate_details">X509-Zertifikat</string>
<string name="trust_certificate_issued_for">Ausgestellt für</string>
<string name="trust_certificate_issued_by">Ausgestellt von</string>
<string name="trust_certificate_validity_period">Gültigkeitszeitraum</string>
<string name="trust_certificate_validity_period_value">%1$s – %2$s (wird nicht erzwungen)</string>
<string name="trust_certificate_fingerprints">Prüfsummen</string>
<string name="trust_certificate_fingerprint_verified">Ich habe die vollständige Prüfsumme händisch überprüft.</string>
<string name="trust_certificate_accept">Akzeptieren</string>
<string name="trust_certificate_reject">Ablehnen</string>
<string name="trust_certificate_reset_info">Alle akzeptierten/abgelehnten Zertifikate können in den App-Einstellungen zurückgesetzt werden.</string>