From ddf597e0d3873633cd848bb87fa6e77ece848e46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 6 May 2021 18:40:35 +0200 Subject: [PATCH] invoke x509 verification upon receiving prekey message in rtp session --- .../crypto/axolotl/AxolotlService.java | 144 +++++++++++------- .../xmpp/jingle/JingleRtpConnection.java | 72 ++++++--- .../xmpp/jingle/stanzas/Reason.java | 4 +- 3 files changed, 143 insertions(+), 77 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index c2d111e4b..81bfb8a46 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -8,7 +8,13 @@ import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.whispersystems.libsignal.IdentityKey; @@ -733,58 +739,62 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { axolotlStore.setFingerprintStatus(fingerprint, status); } - private void verifySessionWithPEP(final XmppAxolotlSession session) { + private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); final SignalProtocolAddress address = session.getRemoteAddress(); final IdentityKey identityKey = session.getIdentityKey(); + final Jid jid; try { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.of(address.getName()), address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Pair verification = mXmppConnectionService.getIqParser().verification(packet); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { - try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); - setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored - } - finishBuildingSessionsFromPEP(address); - return; - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); - } - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - }); - } catch (IllegalArgumentException e) { + jid = Jid.of(address.getName()); + } catch (final IllegalArgumentException e) { fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); + return Futures.immediateFuture(session); } + final SettableFuture future = SettableFuture.create(); + final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { + Pair verification = mXmppConnectionService.getIqParser().verification(response); + if (verification != null) { + try { + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); + setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid1 = Jid.of(address.getName()); + Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn); + account.getRoster().getContact(jid1).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + //ignored + } + finishBuildingSessionsFromPEP(address); + future.set(session); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not verify certificate"); + } + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "no verification found"); + } + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + future.set(session); + }); + return future; } private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { @@ -1255,12 +1265,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { ); } - public OmemoVerifiedPayload decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException { + public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); + final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>(); for (final Map.Entry content : omemoVerifiedRtpContentMap.contents.entrySet()) { final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); - final OmemoVerifiedPayload decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from); + final OmemoVerifiedPayload decryptedTransport; + try { + decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); + } catch (CryptoFailedException e) { + return Futures.immediateFailedFuture(e); + } omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), @@ -1268,13 +1284,26 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { ); } processPostponed(); - return new OmemoVerifiedPayload<>( - omemoVerification, - new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) + final ImmutableList> sessionFutures = pepVerificationFutures.build(); + return Futures.transform( + Futures.allAsList(sessionFutures), + sessions -> { + if (Config.REQUIRE_RTP_VERIFICATION) { + for (XmppAxolotlSession session : sessions) { + requireVerification(session); + } + } + return new OmemoVerifiedPayload<>( + omemoVerification, + new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) + ); + + }, + MoreExecutors.directExecutor() ); } - private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from) throws CryptoFailedException { + private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder> pepVerificationFutures) throws CryptoFailedException { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes()); final OmemoVerification omemoVerification = new OmemoVerification(); @@ -1286,14 +1315,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage); - if (Config.REQUIRE_RTP_VERIFICATION) { - requireVerification(session); - } final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); final Integer preKeyId = session.getPreKeyIdAndReset(); if (preKeyId != null) { postponedSessions.add(session); } + if (session.isFresh()) { + pepVerificationFutures.add(putFreshSession(session)); + } else if (Config.REQUIRE_RTP_VERIFICATION) { + pepVerificationFutures.add(Futures.immediateFuture(session)); + } fingerprint.setContent(plaintext.getPlaintext()); omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setSessionFingerprint(plaintext.getFingerprint()); @@ -1512,15 +1543,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return keyTransportMessage; } - private void putFreshSession(XmppAxolotlSession session) { + private ListenableFuture putFreshSession(XmppAxolotlSession session) { sessions.put(session); if (Config.X509_VERIFICATION) { if (session.getIdentityKey() != null) { - verifySessionWithPEP(session); + return verifySessionWithPEP(session); } else { Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); } } + return Futures.immediateFuture(session); } public enum FetchStatus { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3fea3fee6..af1e05497 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -3,6 +3,9 @@ package eu.siacs.conversations.xmpp.jingle; import android.os.SystemClock; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -12,7 +15,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import org.webrtc.EglBase; import org.webrtc.IceCandidate; @@ -243,7 +249,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void receiveTransportInfo(final JinglePacket jinglePacket) { - if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received + if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { respondOk(jinglePacket); final RtpContentMap contentMap; try { @@ -306,22 +313,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private RtpContentMap receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { + private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { final RtpContentMap receivedContentMap = RtpContentMap.of(jinglePacket); if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { - final AxolotlService.OmemoVerifiedPayload omemoVerifiedPayload; - try { - omemoVerifiedPayload = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); - } catch (final CryptoFailedException e) { - throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e); - } - this.omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + this.omemoVerification); - return omemoVerifiedPayload.getPayload(); + final ListenableFuture> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); + return Futures.transform(future, omemoVerifiedPayload -> { + omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + omemoVerification); + return omemoVerifiedPayload.getPayload(); + }, MoreExecutors.directExecutor()); } else if (expectVerification) { throw new SecurityException("DTLS fingerprint was unexpectedly not verifiable"); } else { - return receivedContentMap; + return Futures.immediateFuture(receivedContentMap); } } @@ -340,9 +344,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } return; } - final RtpContentMap contentMap; + final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionInitiate(jinglePacket, rtpContentMap); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { - contentMap = receiveRtpContentMap(jinglePacket, false); contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { @@ -396,9 +414,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web terminateWithOutOfOrder(jinglePacket); return; } - final RtpContentMap contentMap; + final ListenableFuture future = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionAccept(jinglePacket, rtpContentMap); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", throwable); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + private void receiveSessionAccept(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { - contentMap = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { @@ -762,7 +796,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } catch (final WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC"); webRTCWrapper.close(); - sendRetract(Reason.ofException(e)); + sendRetract(Reason.ofThrowable(e)); return; } try { @@ -774,7 +808,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } catch (final Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(e)); webRTCWrapper.close(); - final Reason reason = Reason.ofException(e); + final Reason reason = Reason.ofThrowable(e); if (isInState(targetState)) { sendSessionTerminate(reason); } else { @@ -1010,7 +1044,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return false; } final FingerprintStatus status = id.account.getAxolotlService().getFingerprintTrust(fingerprint); - return status != null && status.getTrust() == FingerprintStatus.Trust.VERIFIED; + return status != null && status.isVerified(); } public synchronized void acceptCall() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 365fda070..0d6e60fde 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -54,8 +54,8 @@ public enum Reason { } } - public static Reason ofException(final Exception e) { - final Throwable root = Throwables.getRootCause(e); + public static Reason ofThrowable(final Throwable throwable) { + final Throwable root = Throwables.getRootCause(throwable); if (root instanceof RuntimeException) { return of((RuntimeException) root); }