use scopes for trust

This commit is contained in:
Daniel Gultsch 2023-03-02 10:10:12 +01:00
parent 9c64f9c24c
commit 177320d8fe
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
3 changed files with 128 additions and 81 deletions

View file

@ -26,7 +26,6 @@ import im.conversations.android.xmpp.ConnectionPool;
import im.conversations.android.xmpp.ConnectionState; import im.conversations.android.xmpp.ConnectionState;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.manager.TrustManager; import im.conversations.android.xmpp.manager.TrustManager;
import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@ -59,26 +58,28 @@ public class SetupViewModel extends AndroidViewModel {
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>(); private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>(); private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>();
private final HashMap<ByteBuffer, Boolean> trustDecisions = new HashMap<>(); private final HashMap<TrustManager.ScopeFingerprint, Boolean> trustDecisions = new HashMap<>();
private final Function<byte[], ListenableFuture<Boolean>> trustDecisionCallback = private final Function<TrustManager.ScopeFingerprint, ListenableFuture<Boolean>>
fingerprint -> { trustDecisionCallback =
final var decision = this.trustDecisions.get(ByteBuffer.wrap(fingerprint)); scopeFingerprint -> {
if (decision != null) { final var decision = this.trustDecisions.get(scopeFingerprint);
LOGGER.info("Using previous trust decision ({})", decision); if (decision != null) {
return Futures.immediateFuture(decision); LOGGER.info("Using previous trust decision ({})", decision);
} return Futures.immediateFuture(decision);
LOGGER.info("Trust decision arrived in UI"); }
final SettableFuture<Boolean> settableFuture = SettableFuture.create(); LOGGER.info("Trust decision arrived in UI");
final var trustDecision = new TrustDecision(fingerprint, settableFuture); final SettableFuture<Boolean> settableFuture = SettableFuture.create();
final var currentOperation = this.currentOperation; final var trustDecision =
if (currentOperation != null) { new TrustDecision(scopeFingerprint, settableFuture);
currentOperation.cancel(false); final var currentOperation = this.currentOperation;
} if (currentOperation != null) {
this.trustDecision.postValue(trustDecision); currentOperation.cancel(false);
this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE)); }
return settableFuture; this.trustDecision.postValue(trustDecision);
}; this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE));
return settableFuture;
};
private final AccountRepository accountRepository; private final AccountRepository accountRepository;
@ -190,9 +191,9 @@ public class SetupViewModel extends AndroidViewModel {
} }
LOGGER.info( LOGGER.info(
"trying to commit trust for fingerprint {}", "trying to commit trust for fingerprint {}",
TrustManager.fingerprint(trustDecision.fingerprint)); TrustManager.fingerprint(trustDecision.scopeFingerprint.fingerprint.array()));
// in case the UI interface hook gets called again before this gets written to DB // in case the UI interface hook gets called again before this gets written to DB
this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), true); this.trustDecisions.put(trustDecision.scopeFingerprint, true);
if (trustDecision.decision.isDone()) { if (trustDecision.decision.isDone()) {
ConnectionPool.getInstance(getApplication()).reconnect(account); ConnectionPool.getInstance(getApplication()).reconnect(account);
LOGGER.info("it was already done. we should reconnect"); LOGGER.info("it was already done. we should reconnect");
@ -209,9 +210,9 @@ public class SetupViewModel extends AndroidViewModel {
} }
LOGGER.info( LOGGER.info(
"Rejecting trust decision for {}", "Rejecting trust decision for {}",
TrustManager.fingerprint(trustDecision.fingerprint)); TrustManager.fingerprint(trustDecision.scopeFingerprint.fingerprint.array()));
trustDecision.decision.set(false); trustDecision.decision.set(false);
this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), false); this.trustDecisions.put(trustDecision.scopeFingerprint, false);
} }
public LiveData<String> getFingerprint() { public LiveData<String> getFingerprint() {
@ -221,7 +222,7 @@ public class SetupViewModel extends AndroidViewModel {
if (td == null) { if (td == null) {
return null; return null;
} else { } else {
return TrustManager.fingerprint(td.fingerprint, 8); return TrustManager.fingerprint(td.scopeFingerprint.fingerprint.array(), 8);
} }
}); });
} }
@ -472,11 +473,12 @@ public class SetupViewModel extends AndroidViewModel {
} }
public static class TrustDecision { public static class TrustDecision {
public final byte[] fingerprint; public final TrustManager.ScopeFingerprint scopeFingerprint;
public final SettableFuture<Boolean> decision; public final SettableFuture<Boolean> decision;
public TrustDecision(byte[] fingerprint, SettableFuture<Boolean> decision) { public TrustDecision(
this.fingerprint = fingerprint; TrustManager.ScopeFingerprint scopeFingerprint, SettableFuture<Boolean> decision) {
this.scopeFingerprint = scopeFingerprint;
this.decision = decision; this.decision = decision;
} }
} }

View file

@ -510,9 +510,10 @@ public class XmppConnection implements Runnable {
} else { } else {
keyManager = new KeyManager[] {new MyKeyManager(context, credential)}; keyManager = new KeyManager[] {new MyKeyManager(context, credential)};
} }
final String scope = String.format("xmpp:%s", account.address.getDomain().toString());
sc.init( sc.init(
keyManager, keyManager,
new X509TrustManager[] {getManager(TrustManager.class)}, new X509TrustManager[] {getManager(TrustManager.class).scopedTrustManager(scope)},
Conversations.SECURE_RANDOM); Conversations.SECURE_RANDOM);
return sc.getSocketFactory(); return sc.getSocketFactory();
} }

View file

@ -3,6 +3,7 @@ package im.conversations.android.xmpp.manager;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
@ -11,6 +12,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import im.conversations.android.AppSettings; import im.conversations.android.AppSettings;
import im.conversations.android.tls.TrustManagers; import im.conversations.android.tls.TrustManagers;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import java.nio.ByteBuffer;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -22,62 +24,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@SuppressLint("CustomX509TrustManager") @SuppressLint("CustomX509TrustManager")
public class TrustManager extends AbstractManager implements X509TrustManager { public class TrustManager extends AbstractManager {
private static final Logger LOGGER = LoggerFactory.getLogger(TrustManager.class); private static final Logger LOGGER = LoggerFactory.getLogger(TrustManager.class);
private final AppSettings appSettings; private final AppSettings appSettings;
private Function<byte[], ListenableFuture<Boolean>> userInterfaceCallback; private Function<ScopeFingerprint, ListenableFuture<Boolean>> userInterfaceCallback;
public TrustManager(final Context context, final XmppConnection connection) { public TrustManager(final Context context, final XmppConnection connection) {
super(context, connection); super(context, connection);
this.appSettings = new AppSettings(context); this.appSettings = new AppSettings(context);
} }
@Override
public void checkClientTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
throw new CertificateException(
"This implementation has no support for client certificates");
}
@Override
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
if (chain.length == 0) {
throw new CertificateException("Certificate chain is zero length");
}
for (final X509Certificate certificate : chain) {
certificate.checkValidity();
}
final boolean isTrustSystemCaStore = appSettings.isTrustSystemCAStore();
if (isTrustSystemCaStore && isServerTrustedInSystemCaStore(chain, authType)) {
return;
}
final X509Certificate certificate = chain[0];
final byte[] fingerprint = Hashing.sha256().hashBytes(certificate.getEncoded()).asBytes();
LOGGER.info("Looking up {} in database", fingerprint(fingerprint));
final var callback = this.userInterfaceCallback;
if (callback == null) {
throw new CertificateException(
"No user interface registered. Can not trust certificate");
}
final ListenableFuture<Boolean> futureDecision = callback.apply(fingerprint);
final boolean decision;
try {
decision = Boolean.TRUE.equals(futureDecision.get(10, TimeUnit.SECONDS));
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
futureDecision.cancel(true);
throw new CertificateException(
"Timeout waiting for user response", Throwables.getRootCause(e));
}
if (decision) {
return;
}
throw new CertificateException("User did not trust certificate");
}
private boolean isServerTrustedInSystemCaStore( private boolean isServerTrustedInSystemCaStore(
final X509Certificate[] chain, final String authType) { final X509Certificate[] chain, final String authType) {
final var trustManager = TrustManagers.getTrustManager(); final var trustManager = TrustManagers.getTrustManager();
@ -92,11 +51,6 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
} }
} }
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public static String fingerprint(final byte[] bytes) { public static String fingerprint(final byte[] bytes) {
return fingerprint(bytes, bytes.length); return fingerprint(bytes, bytes.length);
} }
@ -112,15 +66,105 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
} }
public void setUserInterfaceCallback( public void setUserInterfaceCallback(
final Function<byte[], ListenableFuture<Boolean>> callback) { final Function<ScopeFingerprint, ListenableFuture<Boolean>> callback) {
this.userInterfaceCallback = callback; this.userInterfaceCallback = callback;
} }
public void removeUserInterfaceCallback( public void removeUserInterfaceCallback(
final Function<byte[], ListenableFuture<Boolean>> callback) { final Function<ScopeFingerprint, ListenableFuture<Boolean>> callback) {
if (this.userInterfaceCallback == callback) { if (this.userInterfaceCallback == callback) {
LOGGER.info("Remove user interface callback"); LOGGER.info("Remove user interface callback");
this.userInterfaceCallback = null; this.userInterfaceCallback = null;
} }
} }
public X509TrustManager scopedTrustManager(final String scope) {
return new ScopedTrustManager(scope);
}
public static class ScopeFingerprint {
public final String scope;
public final ByteBuffer fingerprint;
public ScopeFingerprint(final String scope, final byte[] fingerprint) {
this(scope, ByteBuffer.wrap(fingerprint));
}
public ScopeFingerprint(final String scope, final ByteBuffer fingerprint) {
this.scope = scope;
this.fingerprint = fingerprint;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScopeFingerprint that = (ScopeFingerprint) o;
return Objects.equal(scope, that.scope) && Objects.equal(fingerprint, that.fingerprint);
}
@Override
public int hashCode() {
return Objects.hashCode(scope, fingerprint);
}
}
private class ScopedTrustManager implements X509TrustManager {
private final String scope;
private ScopedTrustManager(final String scope) {
this.scope = scope;
}
@Override
public void checkClientTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
throw new CertificateException(
"This implementation has no support for client certificates");
}
@Override
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
if (chain.length == 0) {
throw new CertificateException("Certificate chain is zero length");
}
for (final X509Certificate certificate : chain) {
certificate.checkValidity();
}
final boolean isTrustSystemCaStore = appSettings.isTrustSystemCAStore();
if (isTrustSystemCaStore && isServerTrustedInSystemCaStore(chain, authType)) {
return;
}
final X509Certificate certificate = chain[0];
final byte[] fingerprint =
Hashing.sha256().hashBytes(certificate.getEncoded()).asBytes();
final var scopeFingerprint = new ScopeFingerprint(scope, fingerprint);
LOGGER.info("Looking up {} in database", fingerprint(fingerprint));
final var callback = TrustManager.this.userInterfaceCallback;
if (callback == null) {
throw new CertificateException(
"No user interface registered. Can not trust certificate");
}
final ListenableFuture<Boolean> futureDecision = callback.apply(scopeFingerprint);
final boolean decision;
try {
decision = Boolean.TRUE.equals(futureDecision.get(10, TimeUnit.SECONDS));
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
futureDecision.cancel(true);
throw new CertificateException(
"Timeout waiting for user response", Throwables.getRootCause(e));
}
if (decision) {
return;
}
throw new CertificateException("User did not trust certificate");
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
} }