invoke x509 verification upon receiving prekey message in rtp session

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java   | 134 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java |  72 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java      |   4 
3 files changed, 138 insertions(+), 72 deletions(-)

Detailed changes

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<XmppAxolotlSession> 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<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
-                    if (verification != null) {
+            jid = Jid.of(address.getName());
+        } catch (final IllegalArgumentException e) {
+            fetchStatusMap.put(address, FetchStatus.SUCCESS);
+            finishBuildingSessionsFromPEP(address);
+            return Futures.immediateFuture(session);
+        }
+        final SettableFuture<XmppAxolotlSession> future = SettableFuture.create();
+        final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
+        mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
+            Pair<X509Certificate[], byte[]> 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 {
-                            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");
-                                }
+                            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, "error during verification " + e.getMessage());
+                            Log.d(Config.LOGTAG, "could not verify certificate");
                         }
-                    } else {
-                        Log.d(Config.LOGTAG, "no verification found");
                     }
-                    fetchStatusMap.put(address, FetchStatus.SUCCESS);
-                    finishBuildingSessionsFromPEP(address);
+                } catch (Exception e) {
+                    Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
                 }
-            });
-        } catch (IllegalArgumentException e) {
+            } 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<RtpContentMap> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException {
+    public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
         final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
         final OmemoVerification omemoVerification = new OmemoVerification();
+        final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
         for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
             final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
-            final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from);
+            final OmemoVerifiedPayload<IceUdpTransportInfo> 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<ListenableFuture<XmppAxolotlSession>> 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<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from) throws CryptoFailedException {
+    private OmemoVerifiedPayload<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> 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<XmppAxolotlSession> 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 {

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<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) {
         final RtpContentMap receivedContentMap = RtpContentMap.of(jinglePacket);
         if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) {
-            final AxolotlService.OmemoVerifiedPayload<RtpContentMap> 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<AxolotlService.OmemoVerifiedPayload<RtpContentMap>> 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<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
+        Futures.addCallback(future, new FutureCallback<RtpContentMap>() {
+            @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<RtpContentMap> future = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
+        Futures.addCallback(future, new FutureCallback<RtpContentMap>() {
+            @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() {

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);
         }