From 67f01fbdca678751ff40eb9f5fdc4910f5df8350 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 21 Dec 2018 20:45:44 -0800 Subject: [PATCH] Add cert pinning during pairing --- .../limelight/nvstream/ConnectionContext.java | 3 + .../com/limelight/nvstream/NvConnection.java | 8 ++- .../nvstream/http/ComputerDetails.java | 5 ++ .../com/limelight/nvstream/http/NvHTTP.java | 59 +++++++++++++------ .../nvstream/http/PairingManager.java | 14 ++++- 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/moonlight-common/src/main/java/com/limelight/nvstream/ConnectionContext.java b/moonlight-common/src/main/java/com/limelight/nvstream/ConnectionContext.java index 1ff5af6a..f881624f 100644 --- a/moonlight-common/src/main/java/com/limelight/nvstream/ConnectionContext.java +++ b/moonlight-common/src/main/java/com/limelight/nvstream/ConnectionContext.java @@ -1,9 +1,12 @@ package com.limelight.nvstream; +import java.security.cert.X509Certificate; + import javax.crypto.SecretKey; public class ConnectionContext { public String serverAddress; + public X509Certificate serverCert; public StreamConfiguration streamConfig; public NvConnectionListener connListener; public SecretKey riKey; diff --git a/moonlight-common/src/main/java/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/main/java/com/limelight/nvstream/NvConnection.java index 8aac0b7d..803bb1e5 100644 --- a/moonlight-common/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/moonlight-common/src/main/java/com/limelight/nvstream/NvConnection.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.cert.X509Certificate; import java.util.concurrent.Semaphore; import javax.crypto.KeyGenerator; @@ -33,14 +34,15 @@ public class NvConnection { private static Semaphore connectionAllowed = new Semaphore(1); 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.cryptoProvider = cryptoProvider; this.uniqueId = uniqueId; - + this.context = new ConnectionContext(); this.context.streamConfig = config; + this.context.serverCert = serverCert; try { // This is unique per connection this.context.riKey = generateRiAesKey(); @@ -83,7 +85,7 @@ public class NvConnection { 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(); diff --git a/moonlight-common/src/main/java/com/limelight/nvstream/http/ComputerDetails.java b/moonlight-common/src/main/java/com/limelight/nvstream/http/ComputerDetails.java index 8c38049b..beadb1ad 100644 --- a/moonlight-common/src/main/java/com/limelight/nvstream/http/ComputerDetails.java +++ b/moonlight-common/src/main/java/com/limelight/nvstream/http/ComputerDetails.java @@ -1,5 +1,6 @@ package com.limelight.nvstream.http; +import java.security.cert.X509Certificate; import java.util.UUID; @@ -15,6 +16,7 @@ public class ComputerDetails { public String remoteAddress; public String manualAddress; public String macAddress; + public X509Certificate serverCert; // Transient attributes public State state; @@ -52,6 +54,9 @@ public class ComputerDetails { if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) { this.macAddress = details.macAddress; } + if (details.serverCert != null) { + this.serverCert = details.serverCert; + } this.pairState = details.pairState; this.runningGameId = details.runningGameId; this.rawAppList = details.rawAppList; diff --git a/moonlight-common/src/main/java/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/main/java/com/limelight/nvstream/http/NvHTTP.java index cfb70c71..03c74b67 100644 --- a/moonlight-common/src/main/java/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/main/java/com/limelight/nvstream/http/NvHTTP.java @@ -13,6 +13,7 @@ import java.net.URISyntaxException; import java.security.Principal; import java.security.PrivateKey; import java.security.SecureRandom; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.LinkedList; import java.util.ListIterator; @@ -24,6 +25,7 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509KeyManager; @@ -47,7 +49,6 @@ import com.squareup.okhttp.ResponseBody; public class NvHTTP { private String uniqueId; private PairingManager pm; - private String address; public static final int HTTPS_PORT = 47984; public static final int HTTP_PORT = 47989; @@ -63,20 +64,31 @@ public class NvHTTP { private OkHttpClient httpClient = new OkHttpClient(); private OkHttpClient httpClientWithReadTimeout; - private TrustManager[] trustAllCerts; - private KeyManager[] ourKeyman; + private TrustManager[] trustManager; + private KeyManager[] keyManager; - private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) { - trustAllCerts = new TrustManager[] { + private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) { + trustManager = new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } - public void checkClientTrusted(X509Certificate[] certs, String authType) {} - public void checkServerTrusted(X509Certificate[] certs, String authType) {} + 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"); + } + } }}; - ourKeyman = new KeyManager[] { + keyManager = new KeyManager[] { new X509KeyManager() { public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { return "Limelight-RSA"; } @@ -107,11 +119,10 @@ public class NvHTTP { 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.address = address; - - initializeHttpState(cryptoProvider); + + initializeHttpState(serverCert, cryptoProvider); try { // The URI constructor takes care of escaping IPv6 literals @@ -186,8 +197,20 @@ public class NvHTTP { // 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. // We want this because it will throw us into the HTTP case if the client is unpaired. getServerVersion(resp); @@ -246,7 +269,7 @@ public class NvHTTP { // to avoid the SSLv3 fallback that causes connection failures try { SSLContext sc = SSLContext.getInstance("TLSv1"); - sc.init(ourKeyman, trustAllCerts, new SecureRandom()); + sc.init(keyManager, trustManager, new SecureRandom()); client.setSslSocketFactory(sc.getSocketFactory()); } catch (Exception e) { @@ -444,9 +467,9 @@ public class NvHTTP { } return null; } - - public PairingManager.PairState pair(String serverInfo, String pin) throws Exception { - return pm.pair(serverInfo, pin); + + public PairingManager getPairingManager() { + return pm; } public static LinkedList getAppListByReader(Reader r) throws XmlPullParserException, IOException { diff --git a/moonlight-common/src/main/java/com/limelight/nvstream/http/PairingManager.java b/moonlight-common/src/main/java/com/limelight/nvstream/http/PairingManager.java index 819dceaa..d5b24360 100644 --- a/moonlight-common/src/main/java/com/limelight/nvstream/http/PairingManager.java +++ b/moonlight-common/src/main/java/com/limelight/nvstream/http/PairingManager.java @@ -29,6 +29,8 @@ public class PairingManager { private X509Certificate cert; private SecretKey aesKey; private byte[] pemCertBytes; + + private X509Certificate serverCert; public enum PairState { NOT_PAIRED, @@ -160,10 +162,14 @@ public class PairingManager { 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 { PairingHashAlgorithm hashAlgo; - + int serverMajorVersion = http.getServerMajorVersion(serverInfo); LimeLog.info("Pairing with server generation: "+serverMajorVersion); if (serverMajorVersion >= 7) { @@ -191,7 +197,9 @@ public class PairingManager { if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) { return PairState.FAILED; } - X509Certificate serverCert = extractPlainCert(getCert); + + // Save this cert for retrieval later for pinning + serverCert = extractPlainCert(getCert); if (serverCert == null) { // Attempting to pair while another device is pairing will cause GFE // to give an empty cert in the response. @@ -271,7 +279,7 @@ public class PairingManager { http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); return PairState.FAILED; } - + return PairState.PAIRED; }