Require cert pinning for HTTPS

This commit is contained in:
Cameron Gutman 2018-12-24 17:48:01 -08:00
parent ecad4aa276
commit 0ddf07f4de
10 changed files with 159 additions and 64 deletions

View File

@ -97,9 +97,14 @@ void MoonlightInstance::NvHTTPInit(int32_t callbackId, pp::VarArray args)
PostMessage(ret);
}
void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, std::string url, bool binaryResponse)
void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, pp::VarArray args)
{
char* _url = strdup(url.c_str());
std::string url = args.Get(0).AsString();
std::string ppkstr = args.Get(1).AsString();
bool binaryResponse = args.Get(2).AsBool();
PostMessage(pp::Var(url.c_str()));
PHTTP_DATA data = http_create_data();
int err;
@ -112,7 +117,7 @@ void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, st
goto clean_data;
}
err = http_request(_url , data);
err = http_request(url.c_str(), ppkstr.c_str(), data);
if (err) {
pp::VarDictionary ret;
ret.Set("callbackId", pp::Var(callbackId));
@ -148,5 +153,4 @@ void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, st
clean_data:
http_free_data(data);
free(_url);
}

View File

@ -27,3 +27,4 @@
#define GS_IO_ERROR -5
#define GS_NOT_SUPPORTED_4K -6
#define GS_CERT_MISMATCH -100

View File

@ -59,7 +59,7 @@ static CURLcode sslctx_function(CURL * curl, void * sslctx, void * parm)
return CURLE_OK;
}
int http_request(char* url, PHTTP_DATA data) {
int http_request(const char* url, const char* ppkstr, PHTTP_DATA data) {
int ret;
CURL *curl;
@ -67,12 +67,10 @@ int http_request(char* url, PHTTP_DATA data) {
if (!curl)
return GS_FAILED;
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl, CURLOPT_SSLENGINE_DEFAULT, 1L);
curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE,"PEM");
curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _write_curl);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, *sslctx_function);
@ -86,8 +84,12 @@ int http_request(char* url, PHTTP_DATA data) {
curl_easy_setopt(curl, CURLOPT_WRITEDATA, data);
curl_easy_setopt(curl, CURLOPT_URL, url);
// HACK: Connecting with TLS v1.2 causes unexpected TLS alerts
curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_1);
// Use the pinned certificate for HTTPS
if (ppkstr != NULL) {
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, ppkstr);
}
if (data->size > 0) {
free(data->memory);
@ -102,7 +104,9 @@ int http_request(char* url, PHTTP_DATA data) {
CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK) {
if (res == CURLE_SSL_PINNEDPUBKEYNOTMATCH) {
ret = GS_CERT_MISMATCH;
} else if (res != CURLE_OK) {
ret = GS_FAILED;
} else if (data->memory == NULL) {
ret = GS_OUT_OF_MEMORY;

View File

@ -31,7 +31,7 @@ typedef struct _HTTP_DATA {
} HTTP_DATA, *PHTTP_DATA;
PHTTP_DATA http_create_data();
int http_request(char* url, PHTTP_DATA data);
int http_request(const char* url, const char* ppkstr, PHTTP_DATA data);
void http_free_data(PHTTP_DATA data);
#ifdef __cplusplus

View File

@ -54,11 +54,13 @@ static int xml_search(char* data, size_t len, char* node, char** result) {
startOffset = strstr(data, startTag);
if (startOffset == NULL) {
free(data);
return GS_FAILED;
}
endOffset = strstr(data, endTag);
if (endOffset == NULL) {
free(data);
return GS_FAILED;
}
@ -67,6 +69,7 @@ static int xml_search(char* data, size_t len, char* node, char** result) {
*result = malloc(strlen(startOffset + strlen(startTag)) + 1);
strcpy(*result, startOffset + strlen(startTag));
free(data);
return GS_OK;
}
@ -141,24 +144,73 @@ static bool verifySignature(const unsigned char *data, int dataLength, unsigned
}
X509* get_cert(PHTTP_DATA data) {
char *result;
char *pemcerthex;
if (xml_search(data->memory, data->size, "plaincert", &result) != GS_OK)
if (xml_search(data->memory, data->size, "plaincert", &pemcerthex) != GS_OK)
return NULL;
BIO* bio = BIO_new_mem_buf(result, -1);
free(result);
// Convert cert from hex string to the PEM string and null terminate
int hexstrlen = strlen(pemcerthex);
char *pemcert = malloc(hexstrlen / 2 + 1);
for (int count = 0; count < hexstrlen; count += 2) {
sscanf(&pemcerthex[count], "%2hhx", &pemcert[count / 2]);
}
pemcert[hexstrlen / 2] = 0;
free(pemcerthex);
// pemcert is referenced, but NOT copied!
BIO* bio = BIO_new_mem_buf(pemcert, -1);
if (bio) {
X509* cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free_all(bio);
free(pemcert);
return cert;
}
else {
free(pemcert);
return NULL;
}
}
static char* x509_to_curl_ppk_string(X509* x509) {
BIO* bio = BIO_new(BIO_s_mem());
// Get x509 public key alone in DER format
EVP_PKEY* pubkey = X509_get_pubkey(x509);
i2d_PUBKEY_bio(bio, pubkey);
EVP_PKEY_free(pubkey);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
// SHA256 hash the resulting DER string
unsigned char pubkeyhash[32];
SHA256((unsigned char*)mem->data, mem->length, pubkeyhash);
BIO_free(bio);
// Base64-encode the resulting SHA256 hash
bio = BIO_new(BIO_s_mem());
BIO* b64 = BIO_new(BIO_f_base64());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
BIO_write(bio, pubkeyhash, sizeof(pubkeyhash));
BIO_flush(bio);
BIO_get_mem_ptr(bio, &mem);
// Assemble the final curl PPK string
const char* prefix = "sha256//";
char* ret = malloc(strlen(prefix) + mem->length + 1);
memcpy(ret, prefix, strlen(prefix));
memcpy(&ret[strlen(prefix)], mem->data, mem->length);
ret[strlen(prefix) + mem->length] = 0;
BIO_free_all(bio);
return ret;
}
int gs_unpair(const char* address) {
int ret = GS_OK;
char url[4096];
@ -167,13 +219,13 @@ int gs_unpair(const char* address) {
return GS_OUT_OF_MEMORY;
snprintf(url, sizeof(url), "http://%s:47989/unpair?uniqueid=%s", address, g_UniqueId);
ret = http_request(url, data);
ret = http_request(url, NULL, data);
http_free_data(data);
return ret;
}
int gs_pair(int serverMajorVersion, const char* address, const char* pin) {
int gs_pair(int serverMajorVersion, const char* address, const char* pin, char** curl_ppk_string) {
int ret = GS_OK;
char* result = NULL;
X509* server_cert = NULL;
@ -188,7 +240,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) {
PHTTP_DATA data = http_create_data();
if (data == NULL)
return GS_OUT_OF_MEMORY;
else if ((ret = http_request(url, data)) != GS_OK)
else if ((ret = http_request(url, NULL, data)) != GS_OK)
goto cleanup;
if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK)
@ -230,7 +282,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) {
bytes_to_hex(challenge_enc, challenge_hex, 16);
snprintf(url, sizeof(url), "http://%s:47989/pair?uniqueid=%s&devicename=roth&updateState=1&clientchallenge=%s", address, g_UniqueId, challenge_hex);
if ((ret = http_request(url, data)) != GS_OK)
if ((ret = http_request(url, NULL, data)) != GS_OK)
goto cleanup;
free(result);
@ -284,7 +336,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) {
bytes_to_hex(challenge_response_hash_enc, challenge_response_hex, 32);
snprintf(url, sizeof(url), "http://%s:47989/pair?uniqueid=%s&devicename=roth&updateState=1&serverchallengeresp=%s", address, g_UniqueId, challenge_response_hex);
if ((ret = http_request(url, data)) != GS_OK)
if ((ret = http_request(url, NULL, data)) != GS_OK)
goto cleanup;
free(result);
@ -328,7 +380,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) {
bytes_to_hex(client_pairing_secret, client_pairing_secret_hex, 16 + 256);
snprintf(url, sizeof(url), "http://%s:47989/pair?uniqueid=%s&devicename=roth&updateState=1&clientpairingsecret=%s", address, g_UniqueId, client_pairing_secret_hex);
if ((ret = http_request(url, data)) != GS_OK)
if ((ret = http_request(url, NULL, data)) != GS_OK)
goto cleanup;
free(result);
@ -341,6 +393,8 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) {
goto cleanup;
}
*curl_ppk_string = x509_to_curl_ppk_string(server_cert);
cleanup:
if (ret != GS_OK)
gs_unpair(address);

View File

@ -23,7 +23,7 @@
extern "C" {
#endif
int gs_pair(int serverMajorVersion, const char* address, const char* pin);
int gs_pair(int serverMajorVersion, const char* address, const char* pin, char** server_cert_der_string);
#ifdef __cplusplus
}

View File

@ -257,13 +257,8 @@ void MoonlightInstance::HandleStopStream(int32_t callbackId, pp::VarArray args)
}
void MoonlightInstance::HandleOpenURL(int32_t callbackId, pp::VarArray args) {
std::string url = args.Get(0).AsString();
bool binaryResponse = args.Get(1).AsBool();
m_HttpThreadPool[m_HttpThreadPoolSequence++ % HTTP_HANDLER_THREADS]->message_loop().PostWork(
m_CallbackFactory.NewCallback(&MoonlightInstance::NvHTTPRequest, callbackId, url, binaryResponse));
PostMessage(pp::Var (url.c_str()));
m_CallbackFactory.NewCallback(&MoonlightInstance::NvHTTPRequest, callbackId, args));
}
void MoonlightInstance::HandlePair(int32_t callbackId, pp::VarArray args) {
@ -272,12 +267,21 @@ void MoonlightInstance::HandlePair(int32_t callbackId, pp::VarArray args) {
}
void MoonlightInstance::PairCallback(int32_t /*result*/, int32_t callbackId, pp::VarArray args) {
int err = gs_pair(atoi(args.Get(0).AsString().c_str()), args.Get(1).AsString().c_str(), args.Get(2).AsString().c_str());
char* ppkstr;
int err = gs_pair(atoi(args.Get(0).AsString().c_str()), args.Get(1).AsString().c_str(), args.Get(2).AsString().c_str(), &ppkstr);
pp::VarDictionary ret;
ret.Set("callbackId", pp::Var(callbackId));
ret.Set("type", pp::Var("resolve"));
ret.Set("ret", pp::Var(err));
if (err == 0) {
ret.Set("type", pp::Var("resolve"));
ret.Set("ret", pp::Var(ppkstr));
free(ppkstr);
}
else {
ret.Set("type", pp::Var("reject"));
ret.Set("ret", pp::Var(err));
}
PostMessage(ret);
}

View File

@ -156,7 +156,7 @@ class MoonlightInstance : public pp::Instance, public pp::MouseLock {
void LoadCert(const char* certStr, const char* keyStr);
void NvHTTPInit(int32_t callbackId, pp::VarArray args);
void NvHTTPRequest(int32_t, int32_t callbackId, std::string url, bool binaryResponse);
void NvHTTPRequest(int32_t, int32_t callbackId, pp::VarArray args);
private:
static CONNECTION_LISTENER_CALLBACKS s_ClCallbacks;

View File

@ -237,6 +237,7 @@ function moduleDidLoad() {
revivedHost.serverUid = hosts[hostUID].serverUid;
revivedHost.externalIP = hosts[hostUID].externalIP;
revivedHost.hostname = hosts[hostUID].hostname;
revivedHost.ppkstr = hosts[hostUID].ppkstr;
addHostToGrid(revivedHost);
}
console.log('%c[index.js]', 'color: green;', 'Loaded previously connected hosts');
@ -277,26 +278,19 @@ function pairTo(nvhttpHost, onSuccess, onFailure) {
pairingDialog.close();
});
console.log('%c[index.js]', 'color: green;', 'Sending pairing request to ' + nvhttpHost.hostname + ' with random number' + randomNumber);
nvhttpHost.pair(randomNumber).then(function(paired) {
if (!paired) {
if (nvhttpHost.currentGame != 0) {
$('#pairingDialogText').html('Error: ' + nvhttpHost.hostname + ' is busy. Stop streaming to pair.');
} else {
$('#pairingDialogText').html('Error: failed to pair with ' + nvhttpHost.hostname + '.');
}
console.log('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs
onFailure();
return;
}
console.log('%c[index.js]', 'color: green;', 'Sending pairing request to ' + nvhttpHost.hostname + ' with PIN: ' + randomNumber);
nvhttpHost.pair(randomNumber).then(function() {
snackbarLog('Pairing successful');
pairingDialog.close();
onSuccess();
}, function(failedPairing) {
snackbarLog('Failed pairing to: ' + nvhttpHost.hostname);
console.error('%c[index.js]', 'color: green;', 'Pairing failed, and returned:', failedPairing);
console.error('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs
if (nvhttpHost.currentGame != 0) {
$('#pairingDialogText').html('Error: ' + nvhttpHost.hostname + ' is busy. Stop streaming to pair.');
} else {
$('#pairingDialogText').html('Error: failed to pair with ' + nvhttpHost.hostname + '.');
}
console.log('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs
onFailure();
});
});

View File

@ -67,6 +67,7 @@ String.prototype.toHex = function() {
function NvHTTP(address, clientUid, userEnteredAddress = '') {
console.log('%c[utils.js, NvHTTP Object]', 'color: gray;', this);
this.address = address;
this.ppkstr = null;
this.paired = false;
this.currentGame = 0;
this.serverMajorVersion = 0;
@ -111,29 +112,60 @@ function _base64ToArrayBuffer(base64) {
NvHTTP.prototype = {
refreshServerInfo: function() {
if (this.ppkstr == null) {
return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) {
this._parseServerInfo(retHttp);
}.bind(this));
}
// try HTTPS first
return sendMessage('openUrl', [this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) {
return sendMessage('openUrl', [this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(ret) {
if (!this._parseServerInfo(ret)) { // if that fails
// try HTTP as a failover. Useful to clients who aren't paired yet
return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) {
return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) {
this._parseServerInfo(retHttp);
}.bind(this));
}
}.bind(this));
}.bind(this),
function(error) {
if (error == -100) { // GS_CERT_MISMATCH
// Retry over HTTP
console.warn('%c[utils.js, utils.js, refreshServerInfo]', 'color: gray;', 'Certificate mismatch. Retrying over HTTP', this);
return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) {
this._parseServerInfo(retHttp);
}.bind(this));
}
}.bind(this));
},
// refreshes the server info using a given address. This is useful for testing whether we can successfully ping a host at a given address
refreshServerInfoAtAddress: function(givenAddress) {
if (this.ppkstr == null) {
// Use HTTP if we have no pinned cert
return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) {
return this._parseServerInfo(retHttp);
}.bind(this));
}
// try HTTPS first
return sendMessage('openUrl', ['https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) {
return sendMessage('openUrl', ['https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(ret) {
if (!this._parseServerInfo(ret)) { // if that fails
console.log('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Failed to parse serverinfo from HTTPS, falling back to HTTP');
console.log('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Failed to parse serverinfo from HTTPS, falling back to HTTP');
// try HTTP as a failover. Useful to clients who aren't paired yet
return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) {
return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) {
return this._parseServerInfo(retHttp);
}.bind(this));
}
}.bind(this));
}.bind(this),
function(error) {
if (error == -100) { // GS_CERT_MISMATCH
// Retry over HTTP
console.warn('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Certificate mismatch. Retrying over HTTP', this);
return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) {
return this._parseServerInfo(retHttp);
}.bind(this));
}
}.bind(this));
},
// called every few seconds to poll the server for updated info
@ -157,7 +189,7 @@ NvHTTP.prototype = {
// Poll for the app list every 10 successful serverinfo polls.
// Not including the first one to avoid PCs taking a while to show
// as online initially
if (this._pollCount++ % 10 == 1) {
if (this.paired && this._pollCount++ % 10 == 1) {
this.getAppListWithCacheFlush();
}
@ -318,7 +350,7 @@ NvHTTP.prototype = {
},
getAppListWithCacheFlush: function() {
return sendMessage('openUrl', [this._baseUrlHttps + '/applist?' + this._buildUidStr(), false]).then(function(ret) {
return sendMessage('openUrl', [this._baseUrlHttps + '/applist?' + this._buildUidStr(), this.ppkstr, false]).then(function(ret) {
$xml = this._parseXML(ret);
$root = $xml.find("root");
@ -377,6 +409,7 @@ NvHTTP.prototype = {
'/appasset?' + this._buildUidStr() +
'&appid=' + appId +
'&AssetType=2&AssetIdx=0',
this.ppkstr,
true
]).then(function(boxArtBuffer) {
var reader = new FileReader();
@ -405,6 +438,7 @@ NvHTTP.prototype = {
'/appasset?' + this._buildUidStr() +
'&appid=' + appId +
'&AssetType=2&AssetIdx=0',
this.ppkstr,
true
]);
}
@ -423,6 +457,7 @@ NvHTTP.prototype = {
'&surroundAudioInfo=' + surroundAudioInfo +
'&remoteControllersBitmap=' + gamepadMask +
'&gcmap=' + gamepadMask,
this.ppkstr,
false
]);
},
@ -434,12 +469,13 @@ NvHTTP.prototype = {
'&rikey=' + rikey +
'&rikeyid=' + rikeyid +
'&surroundAudioInfo=' + surroundAudioInfo,
this.ppkstr,
false
]);
},
quitApp: function() {
return sendMessage('openUrl', [this._baseUrlHttps + '/cancel?' + this._buildUidStr(), false])
return sendMessage('openUrl', [this._baseUrlHttps + '/cancel?' + this._buildUidStr(), this.ppkstr, false])
// Refresh server info after quitting because it may silently fail if the
// session belongs to a different client.
// TODO: We should probably bubble this up to our caller.
@ -460,14 +496,12 @@ NvHTTP.prototype = {
pair: function(randomNumber) {
return this.refreshServerInfo().then(function() {
if (this.paired)
if (this.paired && this.ppkstr)
return true;
if (this.currentGame != 0)
return false;
return sendMessage('pair', [this.serverMajorVersion.toString(), this.address, randomNumber]).then(function(pairStatus) {
return sendMessage('openUrl', [this._baseUrlHttps + '/pair?uniqueid=' + this.clientUid + '&devicename=roth&updateState=1&phrase=pairchallenge', false]).then(function(ret) {
return sendMessage('pair', [this.serverMajorVersion.toString(), this.address, randomNumber]).then(function(ppkstr) {
this.ppkstr = ppkstr;
return sendMessage('openUrl', [this._baseUrlHttps + '/pair?uniqueid=' + this.clientUid + '&devicename=roth&updateState=1&phrase=pairchallenge', this.ppkstr, false]).then(function(ret) {
$xml = this._parseXML(ret);
this.paired = $xml.find('paired').html() == "1";
return this.paired;