Sunday, 1 December 2019

Secure communications for the SupermarketPlanner


The SupermarketPlanner is running a Raspberry Pi Rest Server. This allows me to create a list on the Windows Client and Sync over the Internet to either another Windows Client or an Android Phone App.
As I can potentially connect these over the Internet, I wanted to setup a secure connection between my clients and server. The obvious solution to this was to setup SSL.

With SSL, a private and public key is created. The public key is distributed in the form of a certificate. This can be used to encrypt a message that can only be decrypted with the private key.
When a client connects to a server this information can be used to setup a secure connection via a hand-shake mechanism, A good explanation can be found here.

For my purposes, since I am running both client and server, I decided on setting up a self-signed certificate. I used OpenSSL on my Raspberry Pi to create this, following the steps below:

Create a Private key for the Certificate Authority
openssl genrsa -des3 -out myCA.key 2048
Create the Certificate Authority cert
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 3650 -out myCA.pem
Create Private key for Certificate
openssl req -new -key jsco-cert.key -out jsco.csr
Create certificate signing request
openssl req -new -key jsco-cert.key -out jsco.csr
Create new certificate, sign against Certificate Authority
openssl x509 -req -in jsco.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out jsco-cert.crt -days 365 -sha256 -extfile config.txt
The DNS name for the server needs to be added to the Subject Alternate Name (SAN), not the CommonName (CN). The CN can be defined in the interactive request but SAN can only be defined in a separate configuration file, example below:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = jsco.hopto.org
view raw config.txt hosted with ❤ by GitHub
Self-signed certificates are not signed by a well known Certificate Authority, browsers will pop up a warning. To overcome this on my machines I added my own CA certificate to the Authorities on Chrome and Edge/IE11
  • Chrome
    •  Settings -> Advanced -> Privacy and Security -> Manage Certificates -> Authorities tab -> Import CA certificate
  • Edge / IE11
    •  Open IE11
    •  Internet Options -> Content -> Certificates
    •  Select Trusted Root Certification Authorities
    •  Click Import and choose the CA certificate to upload
Enabling SSL on the web server is simple, using web.py I just needed to add the following (This for version 0.40):

from cheroot.server import HTTPServer
from cheroot.ssl.builtin import BuiltinSSLAdapter
HTTPServer.ssl_adapter = BuiltinSSLAdapter(
certificate = './Certificates/jsco-cert.crt',
private_key = './Certificates/jsco-cert.key')
As for my client applications, after adding my CA to IE11,  SupermarketPlanner, being a .NET Core app running on Windows worked with no changes other than to use the https url. This worked on both Windows 10 and Windows 8.1 boxes.
using (var client = new HttpClient())
{
var postData = new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("A Key", "Some Payload")
};
var content = new FormUrlEncodedContent(postData);
// _serverUrl can be https://x.y.z:port
var response = await client.PostAsync(_serverUrl, content);
// Do something with response
}

For my Android App this was a bit more complicated. When simply changing the URL I got this Exception:

CertPathValidatorException : Trust Anchor for Certification Path not found

The documentation here: https://developer.android.com/training/articles/security-ssl was what I needed for this problem. This allows me to add my own CA to the trust chain.

I added my CA certificate to the assets/ folder in the Android project. An assets folder is a location for files that are copied as-is to the .apk file. This can then be referenced and navigated as a normal directory using the AssetManager

protected String getData(String... params)
{
// Removed params checks
try
{
if (!params[0].isEmpty())
{
m_restUrl += "?date=" + params[0];
}
URL url = new URL(m_restUrl);
SSLContext sslContext = getSSLContext();
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
try (ByteArrayOutputStream result = new ByteArrayOutputStream())
{
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1)
{
result.write(buffer, 0, length);
}
output = result.toString("UTF-8");
} finally
{
urlConnection.disconnect();
}
} catch (Exception ex)
{
output = "ERROR: " + ex.getMessage();
}
return output;
}
/**
* If we aren't using a public CA for the SSL connection we can trust the self-signed CA
* @return SSLContext that includes self-signed CA
*/
private SSLContext getSSLContext()
{
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Load the CA. I've included in the Assets folder
AssetManager assetManager = m_context.getAssets();
InputStream caInput = assetManager.open("myCA.pem");
Certificate ca;
try
{
ca = cf.generateCertificate(caInput);
}
finally
{
caInput.close();
}
// Create a KeyStore containing our trusted CA
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
return context;
}
catch(Exception ex)
{
return null;
}
}
view raw RestClient.java hosted with ❤ by GitHub

No comments:

Post a Comment