MiniABS 1.1: HTTPS support
- Bundle current root certificates (Let's Encrypt, GTS, Sectigo, DigiCert G2, Amazon, ...) missing from Android 2.x's store (Roots.java, tools/gen_roots.py) - Ask once for unknown certificates, showing the SHA-256 fingerprint, and remember the answer; replaces "accept any certificate" (Trust*, Settings) - Local streaming proxy so Android 2.x's MediaPlayer can play https:// servers, with seeking and a fresh token per request (StreamProxy.java) - Tests over real TLS with good, expired, wrong-host, forged and self-signed certificates (test/make_pki.sh, TrustTest.java) - README: server setup for a legacy TLS 1.0 port Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4fe9eed870
commit
b7e43d7d61
24 files changed
+1326
-88
No files matched your search
@@ -12,17 +12,10 @@ import java.net.ProtocolException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -33,7 +26,7 @@ import android.os.Build;
|
||||
|
||||
/** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */
|
||||
final class Api {
|
||||
static final String VERSION = "1.0";
|
||||
static final String VERSION = "1.1";
|
||||
|
||||
static class ApiException extends IOException {
|
||||
final int code;
|
||||
@@ -52,7 +45,6 @@ final class Api {
|
||||
}
|
||||
|
||||
private static final Object REFRESH_LOCK = new Object();
|
||||
private static SSLSocketFactory trustAllFactory;
|
||||
|
||||
static {
|
||||
// Froyo's HttpURLConnection reuses dead pooled sockets; turn pooling off.
|
||||
@@ -70,7 +62,7 @@ final class Api {
|
||||
conn.setReadTimeout(30000);
|
||||
conn.setUseCaches(false);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
if (conn instanceof HttpsURLConnection && Prefs.get(c).trustAll()) trustAll((HttpsURLConnection) conn);
|
||||
if (conn instanceof HttpsURLConnection) Trust.apply(c, (HttpsURLConnection) conn);
|
||||
setMethod(conn, method);
|
||||
conn.setRequestProperty("User-Agent", "MiniABS/" + VERSION + " (Android " + Build.VERSION.RELEASE + ")");
|
||||
return conn;
|
||||
@@ -80,38 +72,6 @@ final class Api {
|
||||
conn.setRequestMethod(method);
|
||||
}
|
||||
|
||||
private static synchronized SSLSocketFactory trustAllFactory() throws Exception {
|
||||
if (trustAllFactory == null) {
|
||||
TrustManager tm = new X509TrustManager() {
|
||||
public void checkClientTrusted(X509Certificate[] chain, String type) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain, String type) {
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
};
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, new TrustManager[]{tm}, null);
|
||||
trustAllFactory = ctx.getSocketFactory();
|
||||
}
|
||||
return trustAllFactory;
|
||||
}
|
||||
|
||||
private static void trustAll(HttpsURLConnection conn) {
|
||||
try {
|
||||
conn.setSSLSocketFactory(trustAllFactory());
|
||||
conn.setHostnameVerifier(new HostnameVerifier() {
|
||||
public boolean verify(String host, SSLSession session) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH over a plain socket. Android 2.x's HttpURLConnection only sends a body with POST or PUT,
|
||||
* and Audiobookshelf uses PATCH for "mark as played" and for saving progress without a session.
|
||||
@@ -130,16 +90,15 @@ final class Api {
|
||||
sock.setSoTimeout(30000);
|
||||
Socket io = sock;
|
||||
if (https) {
|
||||
boolean trust = p.trustAll();
|
||||
SSLSocketFactory f;
|
||||
try {
|
||||
f = trust ? trustAllFactory() : (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
} catch (Exception e) {
|
||||
throw new SSLException("TLS unavailable");
|
||||
SSLSocket ss = (SSLSocket) Trust.factory(c).createSocket(sock, u.getHost(), port, true);
|
||||
try { // send the host name (SNI) where the phone supports it (Android 2.3+)
|
||||
ss.getClass().getMethod("setHostname", String.class).invoke(ss, u.getHost());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
SSLSocket ss = (SSLSocket) f.createSocket(sock, u.getHost(), port, true);
|
||||
ss.startHandshake();
|
||||
if (!trust && !HttpsURLConnection.getDefaultHostnameVerifier().verify(u.getHost(), ss.getSession())) {
|
||||
java.security.cert.Certificate[] peer = ss.getSession().getPeerCertificates();
|
||||
if (peer.length == 0 || !(peer[0] instanceof java.security.cert.X509Certificate)
|
||||
|| !Trust.hostMatches((java.security.cert.X509Certificate) peer[0], u.getHost())) {
|
||||
ss.close();
|
||||
throw new SSLException("Certificate doesn't match " + u.getHost());
|
||||
}
|
||||
|
||||
@@ -64,9 +64,20 @@ abstract class BaseActivity extends Activity {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (offerTrust(null)) return;
|
||||
toast(Util.describe(e));
|
||||
}
|
||||
|
||||
/** If a request just failed on an unrecognised certificate, ask about it. retry runs after trusting. */
|
||||
protected boolean offerTrust(final Runnable retry) {
|
||||
return TrustPrompt.offer(this, new Runnable() {
|
||||
public void run() {
|
||||
if (retry != null) retry.run();
|
||||
else toast("Certificate trusted. Try again.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ mini player
|
||||
|
||||
protected void setupMiniBar() {
|
||||
|
||||
@@ -178,6 +178,12 @@ public class HomeActivity extends BaseActivity {
|
||||
empty.setVisibility(msg == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private final Runnable retryRefresh = new Runnable() {
|
||||
public void run() {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
private void refresh() {
|
||||
App.invalidate();
|
||||
Covers.clearMemory();
|
||||
@@ -315,6 +321,10 @@ public class HomeActivity extends BaseActivity {
|
||||
return;
|
||||
}
|
||||
latestError = Util.describe(e) + "\n\nTap refresh to try again.";
|
||||
if (offerTrust(retryRefresh)) {
|
||||
if (tab == TAB_LATEST) show(null, null);
|
||||
return;
|
||||
}
|
||||
if (tab == TAB_LATEST) offlineFallback(e);
|
||||
}
|
||||
}.start();
|
||||
@@ -369,6 +379,10 @@ public class HomeActivity extends BaseActivity {
|
||||
return;
|
||||
}
|
||||
showsError = Util.describe(e) + "\n\nTap refresh to try again.";
|
||||
if (offerTrust(retryRefresh)) {
|
||||
if (tab == TAB_SHOWS) show(null, null);
|
||||
return;
|
||||
}
|
||||
if (tab == TAB_SHOWS) offlineFallback(e);
|
||||
}
|
||||
}.start();
|
||||
|
||||
@@ -14,7 +14,7 @@ import android.widget.TextView;
|
||||
|
||||
public class LoginActivity extends Activity {
|
||||
private EditText server, user, pass;
|
||||
private CheckBox useKey, trust;
|
||||
private CheckBox useKey;
|
||||
private TextView status, userLabel, passLabel;
|
||||
private Button connect;
|
||||
|
||||
@@ -26,7 +26,6 @@ public class LoginActivity extends Activity {
|
||||
user = (EditText) findViewById(R.id.user);
|
||||
pass = (EditText) findViewById(R.id.pass);
|
||||
useKey = (CheckBox) findViewById(R.id.use_key);
|
||||
trust = (CheckBox) findViewById(R.id.trust);
|
||||
status = (TextView) findViewById(R.id.status);
|
||||
userLabel = (TextView) findViewById(R.id.user_label);
|
||||
passLabel = (TextView) findViewById(R.id.pass_label);
|
||||
@@ -35,7 +34,6 @@ public class LoginActivity extends Activity {
|
||||
Prefs p = Prefs.get(this);
|
||||
server.setText(p.server());
|
||||
user.setText(p.username());
|
||||
trust.setChecked(p.trustAll());
|
||||
String msg = getIntent().getStringExtra("message");
|
||||
if (msg != null) status.setText(msg);
|
||||
|
||||
@@ -87,7 +85,8 @@ public class LoginActivity extends Activity {
|
||||
p.logout();
|
||||
App.clear();
|
||||
HomeActivity.clearCache();
|
||||
p.setServer(srv, trust.isChecked());
|
||||
p.setServer(srv);
|
||||
Trust.lastRejected = null;
|
||||
new Job<String>() {
|
||||
String work() throws Exception {
|
||||
String version = Api.status(LoginActivity.this, srv);
|
||||
@@ -111,6 +110,11 @@ public class LoginActivity extends Activity {
|
||||
void failed(Exception e) {
|
||||
connect.setEnabled(true);
|
||||
status.setText(Util.describe(e));
|
||||
TrustPrompt.offer(LoginActivity.this, new Runnable() {
|
||||
public void run() {
|
||||
go();
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
if (ep != null && prepared && state != ENDED) syncNow(true);
|
||||
releasePlayer();
|
||||
net.shutdown();
|
||||
StreamProxy.shutdown();
|
||||
try {
|
||||
unregisterReceiver(noisy);
|
||||
} catch (Exception ignored) {
|
||||
@@ -380,6 +381,12 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
});
|
||||
}
|
||||
|
||||
/** Direct URL for http:// servers; a local proxy URL for https:// ones (see StreamProxy). */
|
||||
private static String streamUrl(Context c, String contentUrl) throws java.io.IOException {
|
||||
if (StreamProxy.needed(c)) return StreamProxy.get(c).register(contentUrl);
|
||||
return Api.withToken(c, Api.absolute(c, contentUrl));
|
||||
}
|
||||
|
||||
private Source findSource(Context c, Episode e, double startSec) {
|
||||
Source s = new Source();
|
||||
File f = Downloads.audio(c, e);
|
||||
@@ -405,7 +412,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
s.error = "This episode needs transcoding, which Android 2.x can't play. Try downloading it.";
|
||||
} else {
|
||||
s.contentUrl = cu;
|
||||
s.url = Api.withToken(c, Api.absolute(c, cu));
|
||||
s.url = streamUrl(c, cu);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@@ -603,7 +610,11 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
if (gen != generation) return;
|
||||
resumeMs = pos;
|
||||
playOnPrepared = wasPlaying;
|
||||
prepare(Api.withToken(ctx, Api.absolute(ctx, cu)), msg);
|
||||
try {
|
||||
prepare(streamUrl(ctx, cu), msg);
|
||||
} catch (Exception ex) {
|
||||
fail(Util.describe(ex));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,6 +117,11 @@ public class PodcastActivity extends BaseActivity {
|
||||
}
|
||||
empty.setText(Util.describe(e));
|
||||
empty.setVisibility(View.VISIBLE);
|
||||
offerTrust(new Runnable() {
|
||||
public void run() {
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ final class Prefs {
|
||||
String token() { return s("access"); }
|
||||
String refreshToken() { return s("refresh"); }
|
||||
String mode() { return s("mode"); }
|
||||
boolean trustAll() { return sp.getBoolean("trust_all", false); }
|
||||
boolean wifiOnly() { return sp.getBoolean("wifi_only", true); }
|
||||
boolean covers() { return sp.getBoolean("covers", true); }
|
||||
String libraryId() { return s("library_id"); }
|
||||
@@ -61,8 +60,8 @@ final class Prefs {
|
||||
return MODE_JWT.equals(mode()) && refreshToken().length() > 0;
|
||||
}
|
||||
|
||||
void setServer(String server, boolean trustAll) {
|
||||
sp.edit().putString("server", server).putBoolean("trust_all", trustAll).commit();
|
||||
void setServer(String server) {
|
||||
sp.edit().putString("server", server).remove("trust_all").commit();
|
||||
}
|
||||
|
||||
synchronized void setAuth(String user, String mode, String access, String refresh) {
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.miniabs;
|
||||
|
||||
/** Generated by tools/gen_roots.py from the system CA store. Do not edit. */
|
||||
final class Roots {
|
||||
private Roots() {
|
||||
}
|
||||
|
||||
static final String[] DER_BASE64 = {
|
||||
// ISRG_Root_X1 sha256=96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6
|
||||
"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",
|
||||
// GTS_Root_R1 sha256=d947432abde7b7fa90fc2e6b59101b1280e0e1c7e4e40fa3c6887fff57a7f4cf
|
||||
"MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xl"
|
||||
+ "IFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBH"
|
||||
+ "MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi"
|
||||
+ "MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo27xqLpeXo4xL+Sv2"
|
||||
+ "sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g"
|
||||
+ "4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy"
|
||||
+ "4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4"
|
||||
+ "Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Or"
|
||||
+ "p2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPv"
|
||||
+ "OmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1"
|
||||
+ "/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEa"
|
||||
+ "K0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmm"
|
||||
+ "WJyq+bXmYOfg6LEeQkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRiClmpIgUx"
|
||||
+ "PoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/K"
|
||||
+ "YnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflV"
|
||||
+ "y2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT"
|
||||
+ "6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNb"
|
||||
+ "oTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJw"
|
||||
+ "NQzcmRk13NfIRmPVNnGuV/u3gm3c",
|
||||
// USERTrust_RSA_Certification_Authority sha256=e793c9b02fd8aa13e21c31228accb08119643b749c898964b1746d46c3d4cbd2
|
||||
"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"
|
||||
+ "YvnKkKj9",
|
||||
// DigiCert_Global_Root_G2 sha256=cb3ccbb76031e5e0138f8dd39a23f9de47ffc35e43c1144cea27d46a5ab1cb5f
|
||||
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln"
|
||||
+ "aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0x"
|
||||
+ "MzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT"
|
||||
+ "EHd3dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A"
|
||||
+ "MIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULec"
|
||||
+ "YxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hd"
|
||||
+ "G82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9"
|
||||
+ "g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB"
|
||||
+ "/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9"
|
||||
+ "PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/J"
|
||||
+ "sw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FB"
|
||||
+ "cf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyN"
|
||||
+ "xF1WW6LZZm6zNTflMrY=",
|
||||
// Amazon_Root_CA_1 sha256=8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
|
||||
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMG"
|
||||
+ "QW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UE"
|
||||
+ "BhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC"
|
||||
+ "AQoCggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM9O6II8c+6zf1tRn4"
|
||||
+ "SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mj"
|
||||
+ "UcAwhmahRWa6VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7"
|
||||
+ "LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E"
|
||||
+ "BAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYo"
|
||||
+ "u6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gD"
|
||||
+ "S63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1"
|
||||
+ "bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZs"
|
||||
+ "G4q5WTP468SQvvG5",
|
||||
// Microsoft_RSA_Root_Certificate_Authority_2017 sha256=c741f70f4b2a8d88bf2e71c14122ef53ef10eba0cfa5e64cfa20f418853073e0
|
||||
"MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWlj"
|
||||
+ "cm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw"
|
||||
+ "HhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0"
|
||||
+ "aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIiMA0GCSqGSIb3DQEB"
|
||||
+ "AQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/"
|
||||
+ "xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk"
|
||||
+ "3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XF"
|
||||
+ "WvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fz"
|
||||
+ "nhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IY"
|
||||
+ "ZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV"
|
||||
+ "/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/"
|
||||
+ "Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tN"
|
||||
+ "siMwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og6Lq46Brs"
|
||||
+ "TaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupK"
|
||||
+ "UY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2"
|
||||
+ "qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3"
|
||||
+ "pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB"
|
||||
+ "8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy"
|
||||
+ "4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D"
|
||||
+ "5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E",
|
||||
// SSL.com_Root_Certification_Authority_RSA sha256=85666a562ee0be5ce925c1d8890a6f76a87ec16d4d7d5f29ea7419cf20123b69
|
||||
"MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYD"
|
||||
+ "VQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9u"
|
||||
+ "IEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4"
|
||||
+ "YXMxEDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBSb290IENlcnRp"
|
||||
+ "ZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh"
|
||||
+ "4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrz"
|
||||
+ "Qdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+l"
|
||||
+ "SgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9A"
|
||||
+ "hnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyA"
|
||||
+ "KoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/Zd"
|
||||
+ "Fr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRn"
|
||||
+ "hE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV"
|
||||
+ "7jiAJQ2mWTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQEAwIBhjAN"
|
||||
+ "BgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflV"
|
||||
+ "ma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAj"
|
||||
+ "RfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0"
|
||||
+ "cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jb"
|
||||
+ "utIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJ"
|
||||
+ "BjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y"
|
||||
+ "wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShYIc2wBlX7Jz9TkHCp"
|
||||
+ "BB5XJ7k=",
|
||||
// Buypass_Class_2_Root_CA sha256=9a114025197c5bb95d94e63d55cd43790847b646b23cdf11ada4a00eff15fb48
|
||||
"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"
|
||||
+ "4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=",
|
||||
// HARICA_TLS_RSA_Root_CA_2021 sha256=d95d0e8eda79525bf9beb11b14d2100d3294985f0c62d9fabd9cd999eccb7b1d
|
||||
"MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVs"
|
||||
+ "bGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0EgUm9vdCBD"
|
||||
+ "QSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFj"
|
||||
+ "YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJvb3QgQ0EgMjAyMTCC"
|
||||
+ "AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569lmwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FH"
|
||||
+ "ws+RFjZiPTgE4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uva9of08WRiFukiZLRgeaMOVig"
|
||||
+ "1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4MpbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yyk"
|
||||
+ "gmvwKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3bLW7R8pv1GmuebxWMevBLKKAiOIAkbDak"
|
||||
+ "O/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcYAuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqBAGMU"
|
||||
+ "uTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYqE613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLu"
|
||||
+ "bpQzKoNQhArlT4b4UEV4AIHrW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQCoReaDH46+0N"
|
||||
+ "0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLu"
|
||||
+ "MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAUX15QvWiWkKQUEapobQk1"
|
||||
+ "OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar"
|
||||
+ "5AWMxajaH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqPJzLB5UkZv/HywouoCjkxKLR9YjYs"
|
||||
+ "TewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3PzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt"
|
||||
+ "jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzP"
|
||||
+ "rjoiUevRi7PzKzMHVIf6tLITe7pTBGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79aPib8qXP"
|
||||
+ "MThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YWxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnn"
|
||||
+ "kf3/W9b3raYvAwtt41dU63ZTGI0RmLo=",
|
||||
// Certum_Trusted_Network_CA sha256=5c58468d55f58e497e743982d2b50010b6d165374acf83a7d4a32db768c4408e
|
||||
"MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xv"
|
||||
+ "Z2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVk"
|
||||
+ "IE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0"
|
||||
+ "byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIwIAYDVQQDExlDZXJ0"
|
||||
+ "dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7pt"
|
||||
+ "J11/91sts1rHUV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jU"
|
||||
+ "I4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et"
|
||||
+ "0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5"
|
||||
+ "p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8B"
|
||||
+ "Af8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7e"
|
||||
+ "DDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3"
|
||||
+ "zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssD"
|
||||
+ "xLQqKi2WF+A5VLxI03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=",
|
||||
// Go_Daddy_Root_Certificate_Authority_-_G2 sha256=45140b3247eb9cc8c5b4f0d7b53091f73292089e6e5a63e2749dd3aca9198eda
|
||||
"MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcT"
|
||||
+ "ClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRl"
|
||||
+ "IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdB"
|
||||
+ "cml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UEAxMoR28gRGFkZHkg"
|
||||
+ "Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJ"
|
||||
+ "GKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT"
|
||||
+ "5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlR"
|
||||
+ "JQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdY"
|
||||
+ "q7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqa"
|
||||
+ "hQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7P"
|
||||
+ "dIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD0"
|
||||
+ "2JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPK"
|
||||
+ "f1855eJ1usV2GDPOLPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1",
|
||||
// Starfield_Root_Certificate_Authority_-_G2 sha256=2ce1cb0bf9d2f9e102993fbe215152c3b2dd0cabde1c68e5319b839154dbb7f5
|
||||
"MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcT"
|
||||
+ "ClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290"
|
||||
+ "IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVT"
|
||||
+ "MRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJ"
|
||||
+ "bmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQAD"
|
||||
+ "ggEPADCCAQoCggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdh"
|
||||
+ "l+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNM"
|
||||
+ "Yq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP"
|
||||
+ "4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV"
|
||||
+ "HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+C"
|
||||
+ "hTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu34jd/IroAow57"
|
||||
+ "UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6"
|
||||
+ "nH7PfrHxBy22/L/KpL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZc2T5NnRe"
|
||||
+ "JaH1ZgUufzkVqSr7UIuOhWn0",
|
||||
};
|
||||
}
|
||||
@@ -55,6 +55,26 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
}
|
||||
});
|
||||
|
||||
final Preference pins = findPreference("pins");
|
||||
int n = Trust.pinned(this).size();
|
||||
pins.setSummary(n == 0 ? "None" : n + " (tap to forget)");
|
||||
pins.setEnabled(n > 0);
|
||||
pins.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference pref) {
|
||||
new AlertDialog.Builder(SettingsActivity.this)
|
||||
.setMessage("Forget the certificates you chose to trust? You'll be asked again next time.")
|
||||
.setPositiveButton("Forget", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
Trust.forgetPins(SettingsActivity.this);
|
||||
pins.setSummary("None");
|
||||
pins.setEnabled(false);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null).show();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
findPreference("logout").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference pref) {
|
||||
new AlertDialog.Builder(SettingsActivity.this)
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Lets Android's media player stream from an HTTPS server. The player can't do HTTPS before
|
||||
* Android 3.1 and wouldn't use MiniABS's certificate trust anyway, so it plays
|
||||
* http://127.0.0.1:PORT/<random id> and this proxy fetches the real stream (forwarding seeks as
|
||||
* Range requests) with a fresh sign-in token each time.
|
||||
*/
|
||||
final class StreamProxy implements Runnable {
|
||||
private static StreamProxy instance;
|
||||
|
||||
private final Context c;
|
||||
private final ServerSocket server;
|
||||
private final LinkedHashMap<String, String> routes = new LinkedHashMap<String, String>() { // id -> content URL
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
|
||||
return size() > 20;
|
||||
}
|
||||
};
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
private volatile boolean running = true;
|
||||
|
||||
private StreamProxy(Context c) throws IOException {
|
||||
this.c = c.getApplicationContext();
|
||||
server = new ServerSocket(0, 8, InetAddress.getByName("127.0.0.1"));
|
||||
Thread t = new Thread(this, "MiniABS proxy");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
static synchronized StreamProxy get(Context c) throws IOException {
|
||||
if (instance == null || !instance.running) instance = new StreamProxy(c);
|
||||
return instance;
|
||||
}
|
||||
|
||||
static synchronized void shutdown() {
|
||||
if (instance != null) {
|
||||
instance.running = false;
|
||||
try {
|
||||
instance.server.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when streams need to go through the proxy (an https:// server). */
|
||||
static boolean needed(Context c) {
|
||||
return Prefs.get(c).server().toLowerCase(Locale.US).startsWith("https://");
|
||||
}
|
||||
|
||||
/** Returns a local URL the media player can open for this server-relative content URL. */
|
||||
synchronized String register(String contentUrl) {
|
||||
byte[] b = new byte[12];
|
||||
random.nextBytes(b);
|
||||
StringBuilder id = new StringBuilder();
|
||||
for (byte x : b) id.append(Integer.toHexString((x & 0xff) | 0x100).substring(1));
|
||||
routes.put(id.toString(), contentUrl);
|
||||
// keep an extension so the player can guess the format
|
||||
String path = contentUrl.split("\\?")[0];
|
||||
int dot = path.lastIndexOf('.');
|
||||
String ext = dot > path.lastIndexOf('/') ? path.substring(dot) : "";
|
||||
return "http://127.0.0.1:" + server.getLocalPort() + "/" + id + ext;
|
||||
}
|
||||
|
||||
private synchronized String route(String id) {
|
||||
return routes.get(id);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
while (running) {
|
||||
try {
|
||||
final Socket s = server.accept();
|
||||
Thread t = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
serve(s);
|
||||
}
|
||||
}, "MiniABS proxy conn");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
} catch (IOException e) {
|
||||
if (!running) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void serve(Socket s) {
|
||||
HttpURLConnection up = null;
|
||||
try {
|
||||
s.setSoTimeout(30000);
|
||||
InputStream in = s.getInputStream();
|
||||
OutputStream out = s.getOutputStream();
|
||||
String request = readLine(in);
|
||||
String range = null;
|
||||
String line;
|
||||
while ((line = readLine(in)) != null && line.length() > 0) {
|
||||
int colon = line.indexOf(':');
|
||||
if (colon > 0 && line.substring(0, colon).trim().equalsIgnoreCase("Range")) {
|
||||
range = line.substring(colon + 1).trim();
|
||||
}
|
||||
}
|
||||
String[] parts = request == null ? new String[0] : request.split(" ");
|
||||
boolean head = parts.length > 0 && parts[0].equals("HEAD");
|
||||
String id = parts.length > 1 ? parts[1].replaceFirst("^/", "").replaceFirst("\\..*$", "") : "";
|
||||
String target = route(id);
|
||||
if (target == null) {
|
||||
respond(out, "404 Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
int code = 0;
|
||||
for (int attempt = 0; attempt < 2; attempt++) {
|
||||
Api.ensureFresh(c, 120);
|
||||
up = Api.open(c, Api.withToken(c, Api.absolute(c, target)), head ? "HEAD" : "GET");
|
||||
up.setRequestProperty("Accept-Encoding", "identity");
|
||||
if (range != null) up.setRequestProperty("Range", range);
|
||||
code = Api.responseCode(up);
|
||||
if (code == 401 && attempt == 0 && Prefs.get(c).canRefresh()) {
|
||||
up.disconnect();
|
||||
Api.refresh(c, true);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
StringBuilder h = new StringBuilder();
|
||||
h.append("HTTP/1.1 ").append(code).append(' ').append(reason(code)).append("\r\n");
|
||||
copyHeader(up, h, "Content-Type");
|
||||
copyHeader(up, h, "Content-Length");
|
||||
copyHeader(up, h, "Content-Range");
|
||||
copyHeader(up, h, "Accept-Ranges");
|
||||
copyHeader(up, h, "Last-Modified");
|
||||
h.append("Connection: close\r\n\r\n");
|
||||
out.write(h.toString().getBytes("ISO-8859-1"));
|
||||
if (!head && code < 400) {
|
||||
InputStream body = up.getInputStream();
|
||||
try {
|
||||
byte[] buf = new byte[16384];
|
||||
int n;
|
||||
while (running && (n = body.read(buf)) > 0) out.write(buf, 0, n);
|
||||
} finally {
|
||||
body.close();
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
} catch (Exception e) {
|
||||
// the player closed the connection (seek or stop), or the server went away
|
||||
} finally {
|
||||
if (up != null) up.disconnect();
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyHeader(HttpURLConnection up, StringBuilder h, String name) {
|
||||
String v = up.getHeaderField(name);
|
||||
if (v != null) h.append(name).append(": ").append(v).append("\r\n");
|
||||
}
|
||||
|
||||
private static void respond(OutputStream out, String status) throws IOException {
|
||||
out.write(("HTTP/1.1 " + status + "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").getBytes("ISO-8859-1"));
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private static String reason(int code) {
|
||||
switch (code) {
|
||||
case 200: return "OK";
|
||||
case 206: return "Partial Content";
|
||||
case 401: return "Unauthorized";
|
||||
case 403: return "Forbidden";
|
||||
case 404: return "Not Found";
|
||||
case 416: return "Range Not Satisfiable";
|
||||
default: return "Status";
|
||||
}
|
||||
}
|
||||
|
||||
private static String readLine(InputStream in) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int ch;
|
||||
while ((ch = in.read()) != -1) {
|
||||
if (ch == '\n') break;
|
||||
if (ch != '\r' && sb.length() < 8192) sb.append((char) ch);
|
||||
}
|
||||
return ch == -1 && sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
|
||||
/**
|
||||
* Decides which HTTPS certificates MiniABS accepts:
|
||||
* 1. anything the phone itself trusts;
|
||||
* 2. certificates chaining to the modern roots in {@link Roots} (Android 2.3's list predates Let's Encrypt);
|
||||
* 3. certificates the user chose to trust after checking the fingerprint ("pins").
|
||||
*/
|
||||
final class Trust {
|
||||
private Trust() {
|
||||
}
|
||||
|
||||
/** The last certificate chain that wasn't trusted, so the UI can offer to trust it. */
|
||||
static final class Rejected {
|
||||
final X509Certificate[] chain;
|
||||
final String reason;
|
||||
final long at;
|
||||
|
||||
Rejected(X509Certificate[] chain, String reason) {
|
||||
this.chain = chain;
|
||||
this.reason = reason;
|
||||
this.at = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean expired() {
|
||||
return reason != null && reason.startsWith("expired");
|
||||
}
|
||||
}
|
||||
|
||||
static volatile Rejected lastRejected;
|
||||
|
||||
private static SSLSocketFactory factory;
|
||||
private static List<X509Certificate> bundled;
|
||||
|
||||
// ------------------------------------------------------------------ setup
|
||||
|
||||
static synchronized SSLSocketFactory factory(Context c) throws IOException {
|
||||
if (factory == null) {
|
||||
try {
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, new TrustManager[]{new Manager(c.getApplicationContext())}, null);
|
||||
factory = ctx.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Secure connections aren't available: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
static void apply(Context c, HttpsURLConnection conn) throws IOException {
|
||||
conn.setSSLSocketFactory(factory(c));
|
||||
}
|
||||
|
||||
/** Recently rejected (this attempt), or null. */
|
||||
static Rejected recentRejection() {
|
||||
Rejected r = lastRejected;
|
||||
return r != null && System.currentTimeMillis() - r.at < 60000 ? r : null;
|
||||
}
|
||||
|
||||
private static synchronized List<X509Certificate> bundled() {
|
||||
if (bundled == null) {
|
||||
List<X509Certificate> list = new ArrayList<X509Certificate>();
|
||||
for (String b64 : Roots.DER_BASE64) {
|
||||
try {
|
||||
list.add(parse(Base64.decode(b64, Base64.DEFAULT)));
|
||||
} catch (Exception ignored) {
|
||||
// a root this phone can't parse; skip it
|
||||
}
|
||||
}
|
||||
bundled = list;
|
||||
}
|
||||
return bundled;
|
||||
}
|
||||
|
||||
static X509Certificate parse(byte[] der) throws CertificateException {
|
||||
return (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(der));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ pins
|
||||
|
||||
static List<X509Certificate> pinned(Context c) {
|
||||
List<X509Certificate> out = new ArrayList<X509Certificate>();
|
||||
String s = Prefs.get(c).sp.getString("pins", "");
|
||||
if (s.length() == 0) return out;
|
||||
for (String b64 : s.split(",")) {
|
||||
try {
|
||||
out.add(parse(Base64.decode(b64, Base64.DEFAULT)));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Trusts the top certificate the server sent (its issuing CA, or the certificate itself if self-signed). */
|
||||
static synchronized X509Certificate pin(Context c, X509Certificate[] chain) {
|
||||
X509Certificate top = top(chain);
|
||||
try {
|
||||
String b64 = Base64.encodeToString(top.getEncoded(), Base64.NO_WRAP);
|
||||
String s = Prefs.get(c).sp.getString("pins", "");
|
||||
if (!Arrays.asList(s.split(",")).contains(b64)) {
|
||||
Prefs.get(c).sp.edit().putString("pins", s.length() == 0 ? b64 : s + "," + b64).commit();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
factory = null;
|
||||
lastRejected = null;
|
||||
return top;
|
||||
}
|
||||
|
||||
static synchronized void forgetPins(Context c) {
|
||||
Prefs.get(c).sp.edit().remove("pins").commit();
|
||||
factory = null;
|
||||
}
|
||||
|
||||
/** Walks up the chain as far as the server's own certificates go. */
|
||||
static X509Certificate top(X509Certificate[] chain) {
|
||||
X509Certificate cur = chain[0];
|
||||
for (int step = 0; step < 8; step++) {
|
||||
if (cur.getIssuerX500Principal().equals(cur.getSubjectX500Principal())) return cur; // self-signed
|
||||
X509Certificate next = issuerIn(cur, chain);
|
||||
if (next == null) return cur;
|
||||
cur = next;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ verification
|
||||
|
||||
private static final class Manager implements X509TrustManager {
|
||||
private final Context c;
|
||||
private X509TrustManager system;
|
||||
|
||||
Manager(Context c) {
|
||||
this.c = c;
|
||||
try {
|
||||
TrustManagerFactory f = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
f.init((KeyStore) null);
|
||||
for (TrustManager t : f.getTrustManagers()) {
|
||||
if (t instanceof X509TrustManager) system = (X509TrustManager) t;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// no system trust store: rely on bundled roots and pins
|
||||
}
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (chain == null || chain.length == 0) throw new CertificateException("No certificate");
|
||||
if (system != null) {
|
||||
try {
|
||||
system.checkServerTrusted(chain, authType);
|
||||
return;
|
||||
} catch (CertificateException ignored) {
|
||||
// fall through to our own roots and pins
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
List<X509Certificate> anchors = new ArrayList<X509Certificate>(bundled());
|
||||
anchors.addAll(pinned(c));
|
||||
String why = verify(chain, anchors);
|
||||
if (why == null) return;
|
||||
lastRejected = new Rejected(chain.clone(), why);
|
||||
throw new CertificateException("Certificate not trusted: " + why);
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
throw new CertificateException("Not used");
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if chain[0] chains up to one of the anchors, or a short reason why not.
|
||||
* Every certificate that signs another must be a CA, so a website's own certificate
|
||||
* can't be used to forge one for a different site.
|
||||
*/
|
||||
static String verify(X509Certificate[] chain, List<X509Certificate> anchors) {
|
||||
X509Certificate cur = chain[0];
|
||||
try {
|
||||
cur.checkValidity();
|
||||
} catch (Exception e) {
|
||||
return "expired or not yet valid (check the phone's date)";
|
||||
}
|
||||
for (int step = 0; step < 8; step++) {
|
||||
for (X509Certificate a : anchors) {
|
||||
if (Arrays.equals(encoded(a), encoded(cur))) return null; // trusted as-is
|
||||
}
|
||||
for (X509Certificate a : anchors) {
|
||||
if (a.getSubjectX500Principal().equals(cur.getIssuerX500Principal()) && isCa(a) && signedBy(cur, a)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
X509Certificate next = issuerIn(cur, chain);
|
||||
if (next == null) return "unknown issuer";
|
||||
if (!isCa(next)) return "signed by a certificate that isn't a CA";
|
||||
try {
|
||||
next.checkValidity();
|
||||
} catch (Exception e) {
|
||||
return "expired intermediate certificate (check the phone's date)";
|
||||
}
|
||||
cur = next;
|
||||
}
|
||||
return "chain too long";
|
||||
}
|
||||
|
||||
private static X509Certificate issuerIn(X509Certificate cert, X509Certificate[] chain) {
|
||||
for (X509Certificate x : chain) {
|
||||
if (x == cert || Arrays.equals(encoded(x), encoded(cert))) continue;
|
||||
if (x.getSubjectX500Principal().equals(cert.getIssuerX500Principal()) && signedBy(cert, x)) return x;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isCa(X509Certificate x) {
|
||||
if (x.getBasicConstraints() < 0) return false;
|
||||
boolean[] ku = x.getKeyUsage();
|
||||
return ku == null || ku.length <= 5 || ku[5]; // keyCertSign
|
||||
}
|
||||
|
||||
private static boolean signedBy(X509Certificate cert, X509Certificate issuer) {
|
||||
try {
|
||||
cert.verify(issuer.getPublicKey());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encoded(X509Certificate x) {
|
||||
try {
|
||||
return x.getEncoded();
|
||||
} catch (Exception e) {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
/** Standard host name check: DNS/IP subject alternative names (one-label wildcards), else the CN. */
|
||||
static boolean hostMatches(X509Certificate cert, String host) {
|
||||
host = host.toLowerCase(Locale.US);
|
||||
boolean hasDns = false;
|
||||
try {
|
||||
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
|
||||
if (sans != null) {
|
||||
for (List<?> e : sans) {
|
||||
int type = ((Integer) e.get(0)).intValue();
|
||||
String v = String.valueOf(e.get(1)).toLowerCase(Locale.US);
|
||||
if (type == 2) {
|
||||
hasDns = true;
|
||||
if (nameMatches(v, host)) return true;
|
||||
} else if (type == 7 && v.equals(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (hasDns) return false;
|
||||
return nameMatches(commonName(cert.getSubjectX500Principal()).toLowerCase(Locale.US), host);
|
||||
}
|
||||
|
||||
private static boolean nameMatches(String pattern, String host) {
|
||||
if (pattern.startsWith("*.")) {
|
||||
String suffix = pattern.substring(1); // ".example.com"
|
||||
if (host.length() <= suffix.length() || !host.endsWith(suffix)) return false;
|
||||
return host.substring(0, host.length() - suffix.length()).indexOf('.') < 0;
|
||||
}
|
||||
return pattern.equals(host);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ display
|
||||
|
||||
static String fingerprint(X509Certificate x) {
|
||||
try {
|
||||
byte[] d = MessageDigest.getInstance("SHA-256").digest(x.getEncoded());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < d.length; i++) {
|
||||
if (i > 0) sb.append(i == 16 ? "\n" : ":");
|
||||
int v = d[i] & 0xff;
|
||||
sb.append("0123456789ABCDEF".charAt(v >> 4)).append("0123456789ABCDEF".charAt(v & 15));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static String commonName(javax.security.auth.x500.X500Principal p) {
|
||||
String n = p.getName();
|
||||
Matcher m = Pattern.compile("(?:^|,)CN=([^,]+)").matcher(n);
|
||||
if (m.find()) return m.group(1);
|
||||
m = Pattern.compile("(?:^|,)O=([^,]+)").matcher(n);
|
||||
return m.find() ? m.group(1) : n;
|
||||
}
|
||||
|
||||
/** Text for the "trust this server?" dialog. */
|
||||
static String describe(Rejected r, String host) {
|
||||
X509Certificate leaf = r.chain[0];
|
||||
X509Certificate top = top(r.chain);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("This phone doesn't recognise the certificate of ").append(host).append(".\n\n");
|
||||
sb.append("Issued to: ").append(commonName(leaf.getSubjectX500Principal())).append('\n');
|
||||
sb.append("Issued by: ").append(commonName(leaf.getIssuerX500Principal())).append('\n');
|
||||
sb.append("Valid until: ").append(DateFormat.getDateInstance().format(leaf.getNotAfter())).append("\n\n");
|
||||
sb.append("SHA-256 fingerprint:\n").append(fingerprint(leaf)).append("\n\n");
|
||||
sb.append("Only continue if this matches the fingerprint your computer's browser shows for the site.");
|
||||
if (top != leaf) {
|
||||
sb.append("\n\nMiniABS will then trust certificates issued by \"")
|
||||
.append(commonName(top.getSubjectX500Principal())).append("\".");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
/** Asks the user whether to trust a certificate the phone didn't recognise. */
|
||||
final class TrustPrompt {
|
||||
private TrustPrompt() {
|
||||
}
|
||||
|
||||
private static boolean showing;
|
||||
|
||||
/**
|
||||
* If the last connection failed on an unrecognised certificate, shows its details and offers to trust it.
|
||||
* Returns true if the prompt was shown.
|
||||
*/
|
||||
static boolean offer(final Activity a, final Runnable retry) {
|
||||
final Trust.Rejected r = Trust.recentRejection();
|
||||
if (r == null || r.expired() || showing || a.isFinishing()) return false;
|
||||
String host;
|
||||
try {
|
||||
host = new URL(Prefs.get(a).server()).getHost();
|
||||
} catch (Exception e) {
|
||||
host = "the server";
|
||||
}
|
||||
showing = true;
|
||||
new AlertDialog.Builder(a)
|
||||
.setTitle("Check this certificate")
|
||||
.setMessage(Trust.describe(r, host))
|
||||
.setPositiveButton("Trust", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
Trust.pin(a, r.chain);
|
||||
if (retry != null) retry.run();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setOnCancelListener(null)
|
||||
.show()
|
||||
.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
public void onDismiss(DialogInterface d) {
|
||||
showing = false;
|
||||
Trust.lastRejected = null;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -97,11 +97,18 @@ final class Util {
|
||||
if (e instanceof SocketTimeoutException) return "The server took too long to answer.";
|
||||
if (e instanceof ConnectException) return "Can't connect to the server. Is it running and reachable?";
|
||||
if (e instanceof SSLException) {
|
||||
return "Secure (HTTPS) connection failed. Android 2.x can't use modern HTTPS: try your server's local http:// address.";
|
||||
Trust.Rejected r = Trust.recentRejection();
|
||||
if (r != null) {
|
||||
return r.expired() ? "The server's certificate has expired, or the phone's date is wrong."
|
||||
: "This phone doesn't recognise the server's certificate.";
|
||||
}
|
||||
return "Couldn't set up a secure connection. Android 2.3 only speaks TLS 1.0, so the server needs "
|
||||
+ "a legacy HTTPS port (see README). On your home network, the http:// address works too.";
|
||||
}
|
||||
if (e instanceof SocketException) return "Connection dropped. Check your network.";
|
||||
if (e instanceof FileNotFoundException) return "Not found on the server.";
|
||||
String m = e.getMessage();
|
||||
if (m != null && m.contains("not verified")) return "The server's certificate is for a different address.";
|
||||
return m == null || m.length() == 0 ? e.getClass().getSimpleName() : m;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user