Merge pull request #690 from mar-v-in/hkps-by-default

HKPS by default
This commit is contained in:
Dominik Schürmann 2014-06-25 08:48:40 +02:00
commit 150090852b
6 changed files with 245 additions and 91 deletions

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFizCCA3OgAwIBAgIJAK9zyLTPn4CPMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNV
BAYTAk5PMQ0wCwYDVQQIDARPc2xvMR4wHAYDVQQKDBVza3Mta2V5c2VydmVycy5u
ZXQgQ0ExHjAcBgNVBAMMFXNrcy1rZXlzZXJ2ZXJzLm5ldCBDQTAeFw0xMjEwMDkw
MDMzMzdaFw0yMjEwMDcwMDMzMzdaMFwxCzAJBgNVBAYTAk5PMQ0wCwYDVQQIDARP
c2xvMR4wHAYDVQQKDBVza3Mta2V5c2VydmVycy5uZXQgQ0ExHjAcBgNVBAMMFXNr
cy1rZXlzZXJ2ZXJzLm5ldCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBANdsWy4PXWNUCkS3L//nrd0GqN3dVwoBGZ6w94Tw2jPDPifegwxQozFXkG6I
6A4TK1CJLXPvfz0UP0aBYyPmTNadDinaB9T4jIwd4rnxl+59GiEmqkN3IfPsv5Jj
MkKUmJnvOT0DEVlEaO1UZIwx5WpfprB3mR81/qm4XkAgmYrmgnLXd/pJDAMk7y1F
45b5zWofiD5l677lplcIPRbFhpJ6kDTODXh/XEdtF71EAeaOdEGOvyGDmCO0GWqS
FDkMMPTlieLA/0rgFTcz4xwUYj/cD5e0ZBuSkYsYFAU3hd1cGfBue0cPZaQH2HYx
Qk4zXD8S3F4690fRhr+tki5gyG6JDR67aKp3BIGLqm7f45WkX1hYp+YXywmEziM4
aSbGYhx8hoFGfq9UcfPEvp2aoc8u5sdqjDslhyUzM1v3m3ZGbhwEOnVjljY6JJLx
MxagxnZZSAY424ZZ3t71E/Mn27dm2w+xFRuoy8JEjv1d+BT3eChM5KaNwrj0IO/y
u8kFIgWYA1vZ/15qMT+tyJTfyrNVV/7Df7TNeWyNqjJ5rBmt0M6NpHG7CrUSkBy9
p8JhimgjP5r0FlEkgg+lyD+V79H98gQfVgP3pbJICz0SpBQf2F/2tyS4rLm+49rP
fcOajiXEuyhpcmzgusAj/1FjrtlynH1r9mnNaX4e+rLWzvU5AgMBAAGjUDBOMB0G
A1UdDgQWBBTkwyoJFGfYTVISTpM8E+igjdq28zAfBgNVHSMEGDAWgBTkwyoJFGfY
TVISTpM8E+igjdq28zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAR
OXnYwu3g1ZjHyley3fZI5aLPsaE17cOImVTehC8DcIphm2HOMR/hYTTL+V0G4P+u
gH+6xeRLKSHMHZTtSBIa6GDL03434y9CBuwGvAFCMU2GV8w92/Z7apkAhdLToZA/
X/iWP2jeaVJhxgEcH8uPrnSlqoPBcKC9PrgUzQYfSZJkLmB+3jEa3HKruy1abJP5
gAdQvwvcPpvYRnIzUc9fZODsVmlHVFBCl2dlu/iHh2h4GmL4Da2rRkUMlbVTdioB
UYIvMycdOkpH5wJftzw7cpjsudGas0PARDXCFfGyKhwBRFY7Xp7lbjtU5Rz0Gc04
lPrhDf0pFE98Aw4jJRpFeWMjpXUEaG1cq7D641RpgcMfPFvOHY47rvDTS7XJOaUT
BwRjmDt896s6vMDcaG/uXJbQjuzmmx3W2Idyh3s5SI0GTHb0IwMKYb4eBUIpQOnB
cE77VnCYqKvN1NVYAqhWjXbY7XasZvszCRcOG+W3FqNaHOK/n/0ueb0uijdLan+U
f4p1bjbAox8eAOQS/8a3bzkJzdyBNUKGx1BIK2IBL9bn/HravSDOiNRSnZ/R3l9G
ZauX0tu7IIDlRCILXSyeazu0aj/vdT3YFQXPcvt5Fkf5wiNTo53f72/jYEJd6qph
WrpoKqrwGwTpRUCMhYIUt65hsTxCiJJ5nKe39h46sg==
-----END CERTIFICATE-----

View File

@ -67,10 +67,12 @@ public final class Constants {
public static final String LANGUAGE = "language"; public static final String LANGUAGE = "language";
public static final String FORCE_V3_SIGNATURES = "forceV3Signatures"; public static final String FORCE_V3_SIGNATURES = "forceV3Signatures";
public static final String KEY_SERVERS = "keyServers"; public static final String KEY_SERVERS = "keyServers";
public static final String KEY_SERVERS_DEFAULT_VERSION = "keyServersDefaultVersion";
} }
public static final class Defaults { public static final class Defaults {
public static final String KEY_SERVERS = "pool.sks-keyservers.net, subkeys.pgp.net, pgp.mit.edu"; public static final String KEY_SERVERS = "hkps://hkps.pool.sks-keyservers.net, subkeys.pgp.net, hkps://pgp.mit.edu";
public static final int KEY_SERVERS_VERSION = 2;
} }
public static final class DrawerItems { public static final class DrawerItems {

View File

@ -26,7 +26,8 @@ import android.graphics.drawable.Drawable;
import android.os.Environment; import android.os.Environment;
import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.helper.ContactHelper; import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.helper.TlsHelper;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PRNGFixes; import org.sufficientlysecure.keychain.util.PRNGFixes;
@ -81,6 +82,11 @@ public class KeychainApplication extends Application {
getApplicationContext().getResources().getColor(R.color.emphasis)); getApplicationContext().getResources().getColor(R.color.emphasis));
setupAccountAsNeeded(this); setupAccountAsNeeded(this);
// Update keyserver list as needed
Preferences.getPreferences(this).updateKeyServers();
TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer");
} }
public static void setupAccountAsNeeded(Context context) { public static void setupAccountAsNeeded(Context context) {

View File

@ -169,4 +169,22 @@ public class Preferences {
editor.putString(Constants.Pref.KEY_SERVERS, rawData); editor.putString(Constants.Pref.KEY_SERVERS, rawData);
editor.commit(); editor.commit();
} }
public void updateKeyServers() {
if (mSharedPreferences.getInt(Constants.Pref.KEY_SERVERS_DEFAULT_VERSION, 0) !=
Constants.Defaults.KEY_SERVERS_VERSION) {
String[] servers = getKeyServers();
for (int i = 0; i < servers.length; i++) {
if (servers[i].equals("pool.sks-keyservers.net")) {
servers[i] = "hkps://hkps.pool.sks-keyservers.net";
} else if (servers[i].equals("pgp.mit.edu")) {
servers[i] = "hkps://pgp.mit.edu";
}
}
setKeyServers(servers);
mSharedPreferences.edit()
.putInt(Constants.Pref.KEY_SERVERS_DEFAULT_VERSION, Constants.Defaults.KEY_SERVERS_VERSION)
.commit();
}
}
} }

View File

@ -0,0 +1,129 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.helper;
import android.content.res.AssetManager;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.*;
import java.util.HashMap;
import java.util.Map;
public class TlsHelper {
public static class TlsHelperException extends Exception {
public TlsHelperException(Exception e) {
super(e);
}
}
private static Map<String, byte[]> sStaticCA = new HashMap<String, byte[]>();
public static void addStaticCA(String domain, byte[] certificate) {
sStaticCA.put(domain, certificate);
}
public static void addStaticCA(String domain, AssetManager assetManager, String name) {
try {
InputStream is = assetManager.open(name);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int reads = is.read();
while(reads != -1){
baos.write(reads);
reads = is.read();
}
is.close();
addStaticCA(domain, baos.toByteArray());
} catch (IOException e) {
Log.w(Constants.TAG, e);
}
}
public static URLConnection openConnection(URL url) throws IOException, TlsHelperException {
if (url.getProtocol().equals("https")) {
for (String domain : sStaticCA.keySet()) {
if (url.getHost().endsWith(domain)) {
return openCAConnection(sStaticCA.get(domain), url);
}
}
}
return url.openConnection();
}
/**
* Opens a Connection that will only accept certificates signed with a specific CA and skips common name check.
* This is required for some distributed Keyserver networks like sks-keyservers.net
*
* @param certificate The X.509 certificate used to sign the servers certificate
* @param url Connection target
*/
public static HttpsURLConnection openCAConnection(byte[] certificate, URL url)
throws TlsHelperException, IOException {
try {
// Load CA
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca = cf.generateCertificate(new ByteArrayInputStream(certificate));
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
// Tell the URLConnection to use a SocketFactory from our SSLContext
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
return urlConnection;
} catch (CertificateException e) {
throw new TlsHelperException(e);
} catch (NoSuchAlgorithmException e) {
throw new TlsHelperException(e);
} catch (KeyStoreException e) {
throw new TlsHelperException(e);
} catch (KeyManagementException e) {
throw new TlsHelperException(e);
}
}
}

View File

@ -22,37 +22,23 @@ import de.measite.minidns.Client;
import de.measite.minidns.Question; import de.measite.minidns.Question;
import de.measite.minidns.Record; import de.measite.minidns.Record;
import de.measite.minidns.record.SRV; import de.measite.minidns.record.SRV;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.helper.TlsHelper;
import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -200,48 +186,37 @@ public class HkpKeyserver extends Keyserver {
return mSecure ? "https://" : "http://"; return mSecure ? "https://" : "http://";
} }
private HttpURLConnection openConnection(URL url) throws IOException {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) TlsHelper.openConnection(url);
} catch (TlsHelper.TlsHelperException e) {
Log.w(Constants.TAG, e);
}
if (conn == null) {
conn = (HttpURLConnection) url.openConnection();
}
conn.setConnectTimeout(5000);
conn.setReadTimeout(25000);
return conn;
}
private String query(String request) throws QueryFailedException, HttpError { private String query(String request) throws QueryFailedException, HttpError {
List<String> urls = new ArrayList<String>(); try {
if (mSecure) { URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request);
urls.add(getUrlPrefix() + mHost + ":" + mPort + request); Log.d(Constants.TAG, "hkp keyserver query: " + url);
} else { HttpURLConnection conn = openConnection(url);
InetAddress ips[]; conn.connect();
try { int response = conn.getResponseCode();
ips = InetAddress.getAllByName(mHost); if (response >= 200 && response < 300) {
} catch (UnknownHostException e) { return readAll(conn.getInputStream(), conn.getContentEncoding());
throw new QueryFailedException(e.toString()); } else {
} String data = readAll(conn.getErrorStream(), conn.getContentEncoding());
for (InetAddress ip : ips) { throw new HttpError(response, data);
// Note: This is actually not HTTP 1.1 compliant, as we hide the real "Host" value,
// but Android's HTTPUrlConnection does not support any other way to set
// Socket's remote IP address...
urls.add(getUrlPrefix() + ip.getHostAddress() + ":" + mPort + request);
} }
} catch (IOException e) {
throw new QueryFailedException("querying server(s) for '" + mHost + "' failed");
} }
for (String url : urls) {
try {
Log.d(Constants.TAG, "hkp keyserver query: " + url);
URL realUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(25000);
conn.connect();
int response = conn.getResponseCode();
if (response >= 200 && response < 300) {
return readAll(conn.getInputStream(), conn.getContentEncoding());
} else {
String data = readAll(conn.getErrorStream(), conn.getContentEncoding());
throw new HttpError(response, data);
}
} catch (MalformedURLException e) {
// nothing to do, try next IP
} catch (IOException e) {
// nothing to do, try next IP
}
}
throw new QueryFailedException("querying server(s) for '" + mHost + "' failed");
} }
@Override @Override
@ -341,52 +316,44 @@ public class HkpKeyserver extends Keyserver {
@Override @Override
public String get(String keyIdHex) throws QueryFailedException { public String get(String keyIdHex) throws QueryFailedException {
HttpClient client = new DefaultHttpClient(); String request = "/pks/lookup?op=get&options=mr&search=" + keyIdHex;
Log.d(Constants.TAG, "hkp keyserver get: " + request);
String data;
try { try {
String query = getUrlPrefix() + mHost + ":" + mPort + data = query(request);
"/pks/lookup?op=get&options=mr&search=" + keyIdHex; } catch (HttpError httpError) {
Log.d(Constants.TAG, "hkp keyserver get: " + query); throw new QueryFailedException("not found");
HttpGet get = new HttpGet(query); }
HttpResponse response = client.execute(get); Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { if (matcher.find()) {
throw new QueryFailedException("not found"); return matcher.group(1);
}
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
String data = readAll(is, EntityUtils.getContentCharSet(entity));
Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data);
if (matcher.find()) {
return matcher.group(1);
}
} catch (IOException e) {
// nothing to do, better luck on the next keyserver
} finally {
client.getConnectionManager().shutdown();
} }
return null; return null;
} }
@Override @Override
public void add(String armoredKey) throws AddKeyException { public void add(String armoredKey) throws AddKeyException {
HttpClient client = new DefaultHttpClient();
try { try {
String query = getUrlPrefix() + mHost + ":" + mPort + "/pks/add"; String query = getUrlPrefix() + mHost + ":" + mPort + "/pks/add";
HttpPost post = new HttpPost(query); String params;
Log.d(Constants.TAG, "hkp keyserver add: " + query); try {
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); params = "keytext=" + URLEncoder.encode(armoredKey, "utf8");
nameValuePairs.add(new BasicNameValuePair("keytext", armoredKey)); } catch (UnsupportedEncodingException e) {
post.setEntity(new UrlEncodedFormEntity(nameValuePairs));
HttpResponse response = client.execute(post);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new AddKeyException(); throw new AddKeyException();
} }
Log.d(Constants.TAG, "hkp keyserver add: " + query);
HttpURLConnection connection = openConnection(new URL(query));
connection.setRequestMethod("POST");
connection.addRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Content-Length", Integer.toString(params.getBytes().length));
connection.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
wr.writeBytes(params);
wr.flush();
wr.close();
} catch (IOException e) { } catch (IOException e) {
// nothing to do, better luck on the next keyserver throw new AddKeyException();
} finally {
client.getConnectionManager().shutdown();
} }
} }