Add cert pinning during pairing

This commit is contained in:
Cameron Gutman 2018-12-21 20:45:44 -08:00
parent 02b74fbbc5
commit 67f01fbdca
5 changed files with 65 additions and 24 deletions

View File

@ -1,9 +1,12 @@
package com.limelight.nvstream; package com.limelight.nvstream;
import java.security.cert.X509Certificate;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
public class ConnectionContext { public class ConnectionContext {
public String serverAddress; public String serverAddress;
public X509Certificate serverCert;
public StreamConfiguration streamConfig; public StreamConfiguration streamConfig;
public NvConnectionListener connListener; public NvConnectionListener connListener;
public SecretKey riKey; public SecretKey riKey;

View File

@ -6,6 +6,7 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
@ -33,14 +34,15 @@ public class NvConnection {
private static Semaphore connectionAllowed = new Semaphore(1); private static Semaphore connectionAllowed = new Semaphore(1);
private final boolean isMonkey; private final boolean isMonkey;
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider) public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
{ {
this.host = host; this.host = host;
this.cryptoProvider = cryptoProvider; this.cryptoProvider = cryptoProvider;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
this.context = new ConnectionContext(); this.context = new ConnectionContext();
this.context.streamConfig = config; this.context.streamConfig = config;
this.context.serverCert = serverCert;
try { try {
// This is unique per connection // This is unique per connection
this.context.riKey = generateRiAesKey(); this.context.riKey = generateRiAesKey();
@ -83,7 +85,7 @@ public class NvConnection {
private boolean startApp() throws XmlPullParserException, IOException private boolean startApp() throws XmlPullParserException, IOException
{ {
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, null, cryptoProvider); NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
String serverInfo = h.getServerInfo(); String serverInfo = h.getServerInfo();

View File

@ -1,5 +1,6 @@
package com.limelight.nvstream.http; package com.limelight.nvstream.http;
import java.security.cert.X509Certificate;
import java.util.UUID; import java.util.UUID;
@ -15,6 +16,7 @@ public class ComputerDetails {
public String remoteAddress; public String remoteAddress;
public String manualAddress; public String manualAddress;
public String macAddress; public String macAddress;
public X509Certificate serverCert;
// Transient attributes // Transient attributes
public State state; public State state;
@ -52,6 +54,9 @@ public class ComputerDetails {
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) { if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
this.macAddress = details.macAddress; this.macAddress = details.macAddress;
} }
if (details.serverCert != null) {
this.serverCert = details.serverCert;
}
this.pairState = details.pairState; this.pairState = details.pairState;
this.runningGameId = details.runningGameId; this.runningGameId = details.runningGameId;
this.rawAppList = details.rawAppList; this.rawAppList = details.rawAppList;

View File

@ -13,6 +13,7 @@ import java.net.URISyntaxException;
import java.security.Principal; import java.security.Principal;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.ListIterator; import java.util.ListIterator;
@ -24,6 +25,7 @@ import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509KeyManager;
@ -47,7 +49,6 @@ import com.squareup.okhttp.ResponseBody;
public class NvHTTP { public class NvHTTP {
private String uniqueId; private String uniqueId;
private PairingManager pm; private PairingManager pm;
private String address;
public static final int HTTPS_PORT = 47984; public static final int HTTPS_PORT = 47984;
public static final int HTTP_PORT = 47989; public static final int HTTP_PORT = 47989;
@ -63,20 +64,31 @@ public class NvHTTP {
private OkHttpClient httpClient = new OkHttpClient(); private OkHttpClient httpClient = new OkHttpClient();
private OkHttpClient httpClientWithReadTimeout; private OkHttpClient httpClientWithReadTimeout;
private TrustManager[] trustAllCerts; private TrustManager[] trustManager;
private KeyManager[] ourKeyman; private KeyManager[] keyManager;
private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) { private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
trustAllCerts = new TrustManager[] { trustManager = new TrustManager[] {
new X509TrustManager() { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; return new X509Certificate[0];
} }
public void checkClientTrusted(X509Certificate[] certs, String authType) {} public void checkClientTrusted(X509Certificate[] certs, String authType) {
public void checkServerTrusted(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");
}
}
}}; }};
ourKeyman = new KeyManager[] { keyManager = new KeyManager[] {
new X509KeyManager() { new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes, public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { return "Limelight-RSA"; } Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
@ -107,11 +119,10 @@ public class NvHTTP {
httpClientWithReadTimeout.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS); httpClientWithReadTimeout.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
} }
public NvHTTP(String address, String uniqueId, String deviceName, LimelightCryptoProvider cryptoProvider) throws IOException { public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException {
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
this.address = address;
initializeHttpState(serverCert, cryptoProvider);
initializeHttpState(cryptoProvider);
try { try {
// The URI constructor takes care of escaping IPv6 literals // The URI constructor takes care of escaping IPv6 literals
@ -186,8 +197,20 @@ public class NvHTTP {
// //
try { try {
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true); try {
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
} catch (SSLHandshakeException e) {
// Detect if we failed due to a server cert mismatch
if (e.getCause() instanceof CertificateException) {
// Jump to the GfeHttpResponseException exception handler to retry
// over HTTP which will allow us to pair again to update the cert
throw new GfeHttpResponseException(401, "Server certificate mismatch");
}
else {
throw e;
}
}
// This will throw an exception if the request came back with a failure status. // 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. // We want this because it will throw us into the HTTP case if the client is unpaired.
getServerVersion(resp); getServerVersion(resp);
@ -246,7 +269,7 @@ public class NvHTTP {
// to avoid the SSLv3 fallback that causes connection failures // to avoid the SSLv3 fallback that causes connection failures
try { try {
SSLContext sc = SSLContext.getInstance("TLSv1"); SSLContext sc = SSLContext.getInstance("TLSv1");
sc.init(ourKeyman, trustAllCerts, new SecureRandom()); sc.init(keyManager, trustManager, new SecureRandom());
client.setSslSocketFactory(sc.getSocketFactory()); client.setSslSocketFactory(sc.getSocketFactory());
} catch (Exception e) { } catch (Exception e) {
@ -444,9 +467,9 @@ public class NvHTTP {
} }
return null; return null;
} }
public PairingManager.PairState pair(String serverInfo, String pin) throws Exception { public PairingManager getPairingManager() {
return pm.pair(serverInfo, pin); return pm;
} }
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException { public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {

View File

@ -29,6 +29,8 @@ public class PairingManager {
private X509Certificate cert; private X509Certificate cert;
private SecretKey aesKey; private SecretKey aesKey;
private byte[] pemCertBytes; private byte[] pemCertBytes;
private X509Certificate serverCert;
public enum PairState { public enum PairState {
NOT_PAIRED, NOT_PAIRED,
@ -160,10 +162,14 @@ public class PairingManager {
return PairState.PAIRED; return PairState.PAIRED;
} }
public X509Certificate getPairedCert() {
return serverCert;
}
public PairState pair(String serverInfo, String pin) throws MalformedURLException, IOException, XmlPullParserException, CertificateException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException { public PairState pair(String serverInfo, String pin) throws MalformedURLException, IOException, XmlPullParserException, CertificateException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException {
PairingHashAlgorithm hashAlgo; PairingHashAlgorithm hashAlgo;
int serverMajorVersion = http.getServerMajorVersion(serverInfo); int serverMajorVersion = http.getServerMajorVersion(serverInfo);
LimeLog.info("Pairing with server generation: "+serverMajorVersion); LimeLog.info("Pairing with server generation: "+serverMajorVersion);
if (serverMajorVersion >= 7) { if (serverMajorVersion >= 7) {
@ -191,7 +197,9 @@ public class PairingManager {
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) { if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
return PairState.FAILED; return PairState.FAILED;
} }
X509Certificate serverCert = extractPlainCert(getCert);
// Save this cert for retrieval later for pinning
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
// to give an empty cert in the response. // to give an empty cert in the response.
@ -271,7 +279,7 @@ public class PairingManager {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED; return PairState.FAILED;
} }
return PairState.PAIRED; return PairState.PAIRED;
} }