Require cert pinning for HTTPS

This commit is contained in:
Cameron Gutman 2018-12-22 20:12:31 -08:00
parent 67f01fbdca
commit 564e7c71a6
2 changed files with 72 additions and 48 deletions

View File

@ -66,27 +66,36 @@ public class NvHTTP {
private TrustManager[] trustManager; private TrustManager[] trustManager;
private KeyManager[] keyManager; private KeyManager[] keyManager;
private X509Certificate serverCert;
void setServerCert(final X509Certificate serverCert) {
this.serverCert = serverCert;
trustManager = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
if (certs.length != 1) {
throw new CertificateException("Invalid certificate chain length: "+certs.length);
}
// Check the server certificate if we've paired to this host
if (!certs[0].equals(serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
}
};
}
private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) { private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
trustManager = new TrustManager[] { // Set up TrustManager
new X509TrustManager() { setServerCert(serverCert);
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
if (certs.length != 1) {
throw new CertificateException("Invalid certificate chain length: "+certs.length);
}
// Check the server certificate if we've paired to this host
if (serverCert != null && !certs[0].equals(serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
}};
keyManager = new KeyManager[] { keyManager = new KeyManager[] {
new X509KeyManager() { new X509KeyManager() {
@ -187,7 +196,7 @@ public class NvHTTP {
} }
} }
public String getServerInfo() throws MalformedURLException, IOException, XmlPullParserException { public String getServerInfo() throws IOException, XmlPullParserException {
String resp; String resp;
// //
@ -196,38 +205,46 @@ public class NvHTTP {
// like there are extra request headers required to make this stuff work over HTTP. // like there are extra request headers required to make this stuff work over HTTP.
// //
try { // When we have a pinned cert, use HTTPS to fetch serverinfo and fall back on cert mismatch
if (serverCert != null) {
try { try {
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true); try {
} catch (SSLHandshakeException e) { resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
// Detect if we failed due to a server cert mismatch } catch (SSLHandshakeException e) {
if (e.getCause() instanceof CertificateException) { // Detect if we failed due to a server cert mismatch
// Jump to the GfeHttpResponseException exception handler to retry if (e.getCause() instanceof CertificateException) {
// over HTTP which will allow us to pair again to update the cert // Jump to the GfeHttpResponseException exception handler to retry
throw new GfeHttpResponseException(401, "Server certificate mismatch"); // over HTTP which will allow us to pair again to update the cert
} throw new GfeHttpResponseException(401, "Server certificate mismatch");
else { }
throw e; else {
} throw e;
}
}
// This will throw an exception if the request came back with a failure status.
// We want this because it will throw us into the HTTP case if the client is unpaired.
getServerVersion(resp);
}
catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 401) {
// Cert validation error - fall back to HTTP
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
}
// If it's not a cert validation error, throw it
throw e;
} }
// This will throw an exception if the request came back with a failure status. return resp;
// We want this because it will throw us into the HTTP case if the client is unpaired.
getServerVersion(resp);
} }
catch (GfeHttpResponseException e) { else {
if (e.getErrorCode() == 401) { // No pinned cert, so use HTTP
// Cert validation error - fall back to HTTP return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
}
// If it's not a cert validation error, throw it
throw e;
} }
return resp;
} }
public ComputerDetails getComputerDetails() throws MalformedURLException, IOException, XmlPullParserException { public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
String serverInfo = getServerInfo(); String serverInfo = getServerInfo();
@ -285,6 +302,10 @@ public class NvHTTP {
Request request = new Request.Builder().url(url).build(); Request request = new Request.Builder().url(url).build();
Response response; Response response;
if (serverCert == null && !url.startsWith(baseUrlHttp)) {
throw new IllegalStateException("Attempted HTTPS fetch without pinned cert");
}
if (enableReadTimeout) { if (enableReadTimeout) {
performAndroidTlsHack(httpClientWithReadTimeout); performAndroidTlsHack(httpClientWithReadTimeout);
response = httpClientWithReadTimeout.newCall(request).execute(); response = httpClientWithReadTimeout.newCall(request).execute();
@ -552,7 +573,7 @@ public class NvHTTP {
} }
public void unpair() throws IOException { public void unpair() throws IOException {
openHttpConnectionToString(baseUrlHttps + "/unpair?"+buildUniqueIdUuidString(), true); openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true);
} }
public InputStream getBoxArt(NvApp app) throws IOException { public InputStream getBoxArt(NvApp app) throws IOException {

View File

@ -198,7 +198,7 @@ public class PairingManager {
return PairState.FAILED; return PairState.FAILED;
} }
// Save this cert for retrieval later for pinning // Save this cert for retrieval later
serverCert = extractPlainCert(getCert); serverCert = extractPlainCert(getCert);
if (serverCert == null) { if (serverCert == null) {
// Attempting to pair while another device is pairing will cause GFE // Attempting to pair while another device is pairing will cause GFE
@ -206,6 +206,9 @@ public class PairingManager {
return PairState.ALREADY_IN_PROGRESS; return PairState.ALREADY_IN_PROGRESS;
} }
// Require this cert for TLS to this host
http.setServerCert(serverCert);
// Generate a random challenge and encrypt it with our AES key // Generate a random challenge and encrypt it with our AES key
byte[] randomChallenge = generateRandomBytes(16); byte[] randomChallenge = generateRandomBytes(16);
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey); byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);