introduced code to verify omemo device keys with x509 certificates.

cleaned up TrustKeysActivity to automatically close if there is nothing to do
This commit is contained in:
Daniel Gultsch 2015-10-16 23:48:42 +02:00
parent fb7359e6a3
commit cfeb67d71d
8 changed files with 190 additions and 65 deletions

View file

@ -1,10 +1,10 @@
package eu.siacs.conversations.crypto.axolotl;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.whispersystems.libaxolotl.AxolotlAddress;
@ -20,11 +20,9 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashMap;
@ -43,12 +41,13 @@ import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class AxolotlService {
public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
@ -71,6 +70,15 @@ public class AxolotlService {
private int numPublishTriesOnEmptyPep = 0;
private boolean pepBroken = false;
@Override
public void onAdvancedStreamFeaturesAvailable(Account account) {
if (account.getXmppConnection().getFeatures().pep()) {
publishBundlesIfNeeded(true, false);
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
}
}
private static class AxolotlAddressMap<T> {
protected Map<String, Map<Integer, T>> map;
protected final Object MAP_LOCK = new Object();
@ -402,7 +410,6 @@ public class AxolotlService {
byte[] signature = verifier.sign();
IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId());
Log.d(Config.LOGTAG,"verification : "+packet.toString());
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@ -565,6 +572,50 @@ public class AxolotlService {
axolotlStore.setFingerprintTrust(fingerprint, trust);
}
private void verifySessionWithPEP(final XmppAxolotlSession session, final IdentityKey identityKey) {
final AxolotlAddress address = session.getRemoteAddress();
try {
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(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) {
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");
Log.d(Config.LOGTAG, "verified session with x.509 signature");
setFingerprintTrust(session.getFingerprint(), XmppAxolotlSession.Trust.TRUSTED);
} 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, " unable to parse verification");
}
finishBuildingSessionsFromPEP(address);
}
});
} catch (InvalidJidException e) {
finishBuildingSessionsFromPEP(address);
}
}
private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
mXmppConnectionService.keyStatusUpdated();
}
}
private void buildSessionFromPEP(final AxolotlAddress address) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString());
if (address.getDeviceId() == getOwnDeviceId()) {
@ -576,13 +627,6 @@ public class AxolotlService {
Jid.fromString(address.getName()), address.getDeviceId());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
private void finish() {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
mXmppConnectionService.keyStatusUpdated();
}
}
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@ -594,7 +638,7 @@ public class AxolotlService {
if (preKeyBundleList.isEmpty() || bundle == null) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
fetchStatusMap.put(address, FetchStatus.ERROR);
finish();
finishBuildingSessionsFromPEP(address);
return;
}
Random random = new Random();
@ -602,7 +646,7 @@ public class AxolotlService {
if (preKey == null) {
//should never happen
fetchStatusMap.put(address, FetchStatus.ERROR);
finish();
finishBuildingSessionsFromPEP(address);
return;
}
@ -617,17 +661,21 @@ public class AxolotlService {
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
sessions.put(address, session);
fetchStatusMap.put(address, FetchStatus.SUCCESS);
if (Config.X509_VERIFICATION) {
verifySessionWithPEP(session, bundle.getIdentityKey());
} else {
finishBuildingSessionsFromPEP(address);
}
} catch (UntrustedIdentityException | InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
+ e.getClass().getName() + ", " + e.getMessage());
fetchStatusMap.put(address, FetchStatus.ERROR);
finishBuildingSessionsFromPEP(address);
}
finish();
} else {
fetchStatusMap.put(address, FetchStatus.ERROR);
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
finish();
finishBuildingSessionsFromPEP(address);
}
}
});
@ -699,9 +747,9 @@ public class AxolotlService {
return newSessions;
}
public boolean hasPendingKeyFetches(Conversation conversation) {
public boolean hasPendingKeyFetches(Account account, Contact contact) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0);
AxolotlAddress foreignAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);

View file

@ -297,6 +297,9 @@ public class Account extends AbstractEntity {
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
if (xmppConnection != null) {
xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
}
}
public OtrService getOtrService() {

View file

@ -137,9 +137,13 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
if(to != null) {
packet.setTo(to);
return packet;
}
public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION+":"+deviceid, null);
packet.setTo(to);
return packet;
}

View file

@ -3,6 +3,7 @@ package eu.siacs.conversations.parser;
import android.support.annotation.NonNull;
import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
@ -10,6 +11,10 @@ import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@ -204,6 +209,30 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return preKeyRecords;
}
public Pair<X509Certificate[],byte[]> verification(final IqPacket packet) {
Element item = getItem(packet);
Element verification = item != null ? item.findChild("verification",AxolotlService.PEP_PREFIX) : null;
Element chain = verification != null ? verification.findChild("chain") : null;
Element signature = verification != null ? verification.findChild("signature") : null;
if (chain != null && signature != null) {
List<Element> certElements = chain.getChildren();
X509Certificate[] certificates = new X509Certificate[certElements.size()];
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
int i = 0;
for(Element cert : certElements) {
certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(),Base64.DEFAULT)));
++i;
}
return new Pair<>(certificates,Base64.decode(signature.getContent(),Base64.DEFAULT));
} catch (CertificateException e) {
return null;
}
} else {
return null;
}
}
public PreKeyBundle bundle(final IqPacket bundle) {
Element bundleItem = getItem(bundle);
if(bundleItem == null) {

View file

@ -60,6 +60,7 @@ import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
@ -256,7 +257,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
mMessageArchiveService.executePendingQueries(account);
mJingleConnectionManager.cancelInTransmission();
syncDirtyContacts(account);
account.getAxolotlService().publishBundlesIfNeeded(true, false);
}
};
private OnStatusChanged statusListener = new OnStatusChanged() {
@ -459,7 +459,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final String action = intent == null ? null : intent.getAction();
boolean interactive = false;
if (action != null) {
Log.d(Config.LOGTAG, "action: " + action);
switch (action) {
case ConnectivityManager.CONNECTIVITY_ACTION:
if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
@ -760,6 +759,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
connection.setOnBindListener(this.mOnBindListener);
connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
AxolotlService axolotlService = account.getAxolotlService();
if (axolotlService != null) {
connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
}
return connection;
}
@ -1066,8 +1069,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void run() {
Log.d(Config.LOGTAG, "restoring roster");
for (Account account : accounts) {
databaseBackend.readRoster(account.getRoster());
account.initAccountServices(XmppConnectionService.this);
databaseBackend.readRoster(account.getRoster());
}
getBitmapCache().evictAll();
Looper.prepare();

View file

@ -8,6 +8,7 @@ import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.whispersystems.libaxolotl.IdentityKey;
@ -16,6 +17,7 @@ import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@ -27,11 +29,11 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
private Jid accountJid;
private Jid contactJid;
private boolean hasOtherTrustedKeys = false;
private boolean hasPendingFetches = false;
private boolean hasNoTrustedKeys = true;
private Contact contact;
private Account mAccount;
private TextView keyErrorMessage;
private LinearLayout keyErrorMessageCard;
private TextView ownKeysTitle;
@ -50,10 +52,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
@Override
public void onClick(View v) {
commitTrusts();
Intent data = new Intent();
data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
setResult(RESULT_OK, data);
finish();
finishOk();
}
};
@ -157,11 +156,11 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
foreignKeysTitle.setText(contactJid.toString());
foreignKeysCard.setVisibility(View.VISIBLE);
}
if(hasPendingFetches) {
if(hasPendingKeyFetches()) {
setFetching();
lock();
} else {
if (!hasForeignKeys && !hasOtherTrustedKeys) {
if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
keyErrorMessageCard.setVisibility(View.VISIBLE);
keyErrorMessage.setText(R.string.error_no_keys_to_trust);
ownKeys.removeAllViews(); ownKeysCard.setVisibility(View.GONE);
@ -172,12 +171,13 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
}
private void getFingerprints(final Account account) {
Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
private boolean reloadFingerprints() {
AxolotlService service = this.mAccount.getAxolotlService();
Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
if (hasNoTrustedKeys) {
ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED));
foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
ownKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED));
foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
}
for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) {
@ -189,39 +189,55 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
foreignKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
}
}
return ownKeysSet.size() + foreignKeysSet.size() > 0;
}
@Override
public void onBackendConnected() {
if ((accountJid != null) && (contactJid != null)) {
final Account account = xmppConnectionService
.findAccountByJid(accountJid);
if (account == null) {
this.mAccount = xmppConnectionService.findAccountByJid(accountJid);
if (this.mAccount == null) {
return;
}
this.contact = account.getRoster().getContact(contactJid);
this.contact = this.mAccount.getRoster().getContact(contactJid);
ownKeysToTrust.clear();
foreignKeysToTrust.clear();
getFingerprints(account);
if(account.getAxolotlService().getNumTrustedKeys(contact) > 0) {
hasOtherTrustedKeys = true;
}
Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false);
if(account.getAxolotlService().hasPendingKeyFetches(conversation)) {
hasPendingFetches = true;
}
reloadFingerprints();
populateView();
}
}
private boolean hasNoOtherTrustedKeys() {
return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
}
private boolean hasPendingKeyFetches() {
return mAccount != null && contact != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount,contact);
}
@Override
public void onKeyStatusUpdated() {
final Account account = xmppConnectionService.findAccountByJid(accountJid);
hasPendingFetches = false;
getFingerprints(account);
boolean keysToTrust = reloadFingerprints();
if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
refreshUi();
} else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(TrustKeysActivity.this, "Nothing to do", Toast.LENGTH_SHORT).show();
finishOk();
}
});
}
}
private void finishOk() {
Intent data = new Intent();
data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
setResult(RESULT_OK, data);
finish();
}
private void commitTrusts() {
@ -248,7 +264,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
private void lockOrUnlockAsNeeded() {
if (!hasOtherTrustedKeys && !foreignKeysToTrust.values().contains(true)){
if (hasNoOtherTrustedKeys() && !foreignKeysToTrust.values().contains(true)){
lock();
} else {
unlock();

View file

@ -1,16 +1,21 @@
package eu.siacs.conversations.utils;
import android.util.Log;
import android.util.Pair;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.jce.PrincipalUtil;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.cert.X509Extension;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
@ -137,11 +142,26 @@ public final class CryptoHelper {
}
}
public static Pair<Jid,String> extractJidAndName(X509Certificate certificate) throws CertificateEncodingException, InvalidJidException {
public static Pair<Jid,String> extractJidAndName(X509Certificate certificate) throws CertificateEncodingException, InvalidJidException, CertificateParsingException {
Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
List<String> emails = new ArrayList<>();
if (alternativeNames != null) {
for(List<?> san : alternativeNames) {
Integer type = (Integer) san.get(0);
if (type == 1) {
emails.add((String) san.get(1));
}
}
}
X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
//String xmpp = IETFUtils.valueToString(x500name.getRDNs(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.8.5"))[0].getFirst().getValue());
String email = IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue());
if (emails.size() == 0) {
emails.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue()));
}
String name = IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue());
return new Pair<>(Jid.fromString(email),name);
if (emails.size() >= 1) {
return new Pair<>(Jid.fromString(emails.get(0)), name);
} else {
return null;
}
}
}

View file

@ -947,11 +947,10 @@ public class XmppConnection implements Runnable {
}
}
disco.put(jid, info);
if (account.getServer().equals(jid)) {
if ((jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))
&& disco.containsKey(account.getServer())
&& disco.containsKey(account.getJid().toBareJid())) {
enableAdvancedStreamFeatures();
for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
listener.onAdvancedStreamFeaturesAvailable(account);
}
}
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not query disco info for "+jid.toString());
@ -969,6 +968,9 @@ public class XmppConnection implements Runnable {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
}
for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
listener.onAdvancedStreamFeaturesAvailable(account);
}
}
private void sendServiceDiscoveryItems(final Jid server) {