Donnerstag, 27. Juli 2017

Accessing data on a server with a self-signed certificate

Accessing encrypted data on the web is relatively simple. Even the somewhat more complicated API using HttpURLConnection is relatively straightforward:

(Examples in Kotlin)

val url = URL(urlString) 
val conn = url.openConnection() as HttpURLConnection 
conn.requestMethod = "GET" 
conn.connect() 
val data = conn.inputStream.bufferedReader().use { it.readText() }
...
conn.close()

This is even true for encrypted communications via https. The system simply adds another layer doing the encryption and decryption and you can use it the same way you would do with unencrypted traffic.

But there is more happening in the background that can go wrong.

When the encrypted channel is being establish, the server sends a certificate. This certificate contains among others:

  • the host name of the server
  • the public key of the server
  • the period for which the key is valid
  • and usually some signatures from well know certificate authorities (CA)


If the host name in the certificate does not match the one in the URL or the key has expired or is not yet valid Java/Kotlin will raise an exception and refuse to connect.

The problematic part with self-signed certificates is that they don't carry a signature from a well-known CA because they are “self-signed”. Well-known in this context means that the certificate of the CA is present in the Java keystore. A standard connect request will fail.

The way around this is to create a keystore with the self-signed certificate in it and tell Kotlin/Java to use it.

How you do you get this certificate?

If you use a server with a self-signed certificate, chances are that you have installed it yourself. The certificate can be found in that installation.
Or you open the connection in Firefox. The certificate info page lets you export the cert. A certificate looks similar to this:

-----BEGIN CERTIFICATE----- 
MIIEfjCCA2agAwIBAgIIGhqMkdYVlLwwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UE 
cm5ld ... 
...
fv23f6eTYPc1c4zq7kuTC4Uz385+ZisEV+o0+g3pevavfYL+BeOuNAyd1muvP7Ej 
vIE= 
-----END CERTIFICATE-----

Which is essentially a base64 encoded version with a special header and footer.
You could then store that file on your filesystem or in a ressource file on Android, and tell Kotlin to use it instead of the standard keystore (that's what TlsTest.setCertSocketFactory(cert) will do for you) .

But there is a nicer way. As the certificate is sent from the server when the connection is establish, why can't we use it?

The answer is, we can. However when using the standard functions there is a “chicken and egg” problem: You can access the certificate when the connection is established but you need the certificate to make the connection in the first place...

The solve the class shown below implements a “self-signed certificate friendly” TrustManager. The Trust Manager is responsible to check the certificate validity (expiration) and the chain of trust (back to the certificate authorities). This special Trust Manager calls the functions of the original Trust Manager with one exception.  If the chain of trust has a length of 1 (which is true for self-signed certificates) it forgoes checking the chain of trust.

It uses this connection to obtain the certificate. If also provides a method to get some user readable information, so that the user can decide to trust it or not.

A third method can be used to install a keystore with the obtained self-signed certificate before opening the data connection.

Here is a typical use case:

val (cert, except) = TlsTest.testConnection("https://xxxxxxx")
if (except != null) { .. abort, there was a fatal error ... }

print(TlsTest.certInfo(cert))   // possibly asking the user for confirmation
...
val conn = java.net.URL("https://xxxxxx/foo").openConnection() as HttpURLConnection
TlsTest.setCertSocketFactory(conn, cert)
conn.connect()
...
val data = conn.inputStream.bufferedReader().use { it.readText() }

There is one edge case this class does not cover. During its executing no “real” data is being  transferred. Its all done during the establishment phase. If you try to securely connect to a server that only “speaks” http, this mismatch only becomes apparent when data is sent, which then causes a “javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?".

Here is the class – available as a Gist on Github

package de.mstrecke.util
import java.net.HttpURLConnection
import java.net.URL
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.*
/**
* Functions to access data on a server that uses a self-signed certificate
*
* Usage:
*
* // "cert" is the certificate to be used, if any
* // "except" is a fatal exception (no connection, etc.), if any
*
* val (cert, except) = TlsTest.testConnection("https://xxxxxxx")
* if (except != null) { .. abort, there was a fatal error ... }
*
* print(TlsTest.certInfo(cert)) // possibly asking the user for confirmation
* ...
* val conn = java.net.URL("https://xxxxxx/foo").openConnection() as HttpURLConnection
* TlsTest.setCertSocketFactory(conn, cert)
* conn.connect()
* ...
* val data = conn.inputStream.bufferedReader().use { it.readText() }
*
* Note:
* This routine will notice if you try a non-TLS connection on a TLS server.
* However, if you try a TLS connection on a non-TLS server, an exception will not thrown
* until later when the first real data is being exchanged:
* "javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?"
*/
class TlsTest {
/**
* Self-signed certificate friendly TrustManager
*
* Uses the functions of a default TrustManager, except
* - the cert chain is not checked if (and only if) it has a length of 1
*
* Note:
* - expiration dates are checked
* - the host name is checked
*
* Based on https://stackoverflow.com/questions/35545126/an-unsafe-implementation-of-the-interface-x509trustmanager-from-google/35571883#35571883
*/
private class SelfSignedFriendlyTrustManager
/**
* Constructor for SelfSignedFriendlyTrustManager.
*/
@Throws(NoSuchAlgorithmException::class, KeyStoreException::class)
constructor(keystore: KeyStore) : X509TrustManager {
val standardTrustManager: X509TrustManager
init {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
factory.init(keystore)
val trustmanagers = factory.trustManagers
if (trustmanagers.isEmpty()) {
throw NoSuchAlgorithmException("no trust manager found")
}
standardTrustManager = trustmanagers[0] as X509TrustManager
}
@Throws(CertificateException::class)
override fun checkClientTrusted(certificates: Array<X509Certificate>, authType: String) {
standardTrustManager.checkClientTrusted(certificates, authType)
}
@Throws(CertificateException::class)
override fun checkServerTrusted(certificates: Array<X509Certificate>?, authType: String) {
if (certificates?.size == 1) {
// only if cert exists and trust chain length == 1
// check only validity (valid until/not before), disregard trust chain (the "friendly" part
certificates[0].checkValidity()
} else {
standardTrustManager.checkServerTrusted(certificates, authType)
}
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return this.standardTrustManager.acceptedIssuers
}
}
companion object {
/**
* SocketFactory using the SelfSignedFriendlyTrustManager
*
* @return TrustManager that accepts all self-signed certificates
* @note Host name must match, expiration dates are checked
*
* Used only once to obtain the self-signed certificate
*/
private fun createSelfSignedFriendlySocketFactory(): SSLSocketFactory {
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType)
keyStore.load(null, null)
// create trustManager with self-signed friendly TrustManager
val trustManagers: Array<out TrustManager> = arrayOf(SelfSignedFriendlyTrustManager(keyStore))
val context = SSLContext.getInstance("TLS")
context.init(null, trustManagers, null)
return context.socketFactory
}
/**
* Create SocketFactory that only excepts connection secured with the specified certificate
*
* @property cert_PEM certificate in PEM format
* @return SSLSocketFactory that only accepts the cert_PEM certificate
*/
private fun createSingleCASocketFactory(cert_PEM: String): SSLSocketFactory {
val cf = CertificateFactory.getInstance("X.509")
val caInput = cert_PEM.byteInputStream()
val ca = caInput.use { // evaluates to null in case of errors
cf.generateCertificate(it)
}
// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType)
keyStore.load(null, null)
keyStore.setCertificateEntry("ca", ca)
// Create a TrustManager that trusts the CAs in our KeyStore
val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm)
tmf.init(keyStore)
// Create an SSLContext that uses our TrustManager
val context = SSLContext.getInstance("TLS")
context.init(null, tmf.trustManagers, null)
return context.socketFactory
}
/**
* Set sslSocketFactory of the HttpsURLConnection to accept only this cert
*
* @property conn Http(s)URLConnection opened by calling function
* @property cert_PEM certificate to use or null
*/
fun setCertSocketFactory(conn: HttpURLConnection, cert_PEM: String?) {
if (cert_PEM == null) return
if (conn !is HttpsURLConnection) return // Http_s_URLConnection !!
conn.sslSocketFactory = createSingleCASocketFactory(cert_PEM = cert_PEM)
}
/**
* Test connection to URL
*
* @property urlString URL to test
* @property cert_PEM certificate to use or null
* @return server certificate (if we need it) or null / fatal exception (host no found, etc.) or null
*/
fun testConnection(urlString: String, cert_PEM: String? = null): Pair<String?, Exception?> {
val conn = URL(urlString).openConnection() as HttpURLConnection
val isHttps = conn is HttpsURLConnection
conn.requestMethod = "HEAD"
try {
if (isHttps) {
cert_PEM?.let {
(conn as HttpsURLConnection).sslSocketFactory = createSingleCASocketFactory(it)
}
}
conn.connect()
// Try to read - it should return an empty string
// ... or throw a SocketException if we tried http on a https port
// and yes, the compiler will complain that the result is not used anywhere else
val dummy = conn.inputStream.bufferedReader().use { it.readText() }
conn.disconnect()
// If we are here, everything worked, i.e. connection is:
// HTTP, HTTPS with 'normal' cert (validated via the normal trust chain), or HTTPS with supplied cert_PEM
if (isHttps) {
return Pair(cert_PEM, null)
}
return Pair(null, null)
} catch (e: javax.net.ssl.SSLHandshakeException) {
// If we are here the connection is HTTPS
// - without a supplied cert, i.e. we have to download it, or
// - with a supplied cert, i.e. the old one didn't work anymore => try without a cert, and if that fails download the new one
if (cert_PEM != null) {
try {
// Let's check first if we can now access the URL via the normal chain of trust
val conn2 = URL(urlString).openConnection() as HttpsURLConnection
conn2.connect()
conn2.disconnect()
// no error => we don't need a specific cert
return Pair(null, null)
}
catch (e: javax.net.ssl.SSLHandshakeException) {
}
}
// Try again with the self-signed friendly SocketFactory
val conn3 = URL(urlString).openConnection() as HttpsURLConnection
conn3.sslSocketFactory = createSelfSignedFriendlySocketFactory()
try {
conn3.connect()
} catch (e: Exception) {
return Pair(null, e) // e.g. Hostname ... was not verified
}
// If we are here, we got a HTTPS connection to a server with
// a self-signed cert
return try {
// return the certificate in PEM form
//
// outside Android: java.util.Base64.getMimeEncoder().encodeToString(conn3.serverCertificates[0].encoded) +
// Android: android.util.Base64.encodeToString(conn3.serverCertificates[0].encoded, android.util.Base64.DEFAULT) +
Pair("-----BEGIN CERTIFICATE-----\n" +
android.util.Base64.encodeToString(conn3.serverCertificates[0].encoded, android.util.Base64.DEFAULT) +
"-----END CERTIFICATE-----\n", null)
} catch (e: Exception) {
return Pair(null, e)
} finally {
conn3.disconnect()
}
} catch (e: Exception) {
// any other error from first attempt
return Pair(null, e)
}
}
/**
* Get info string for cert_PEM (subject line and SHA256 checksum
*
* @property cert_PEM certificate in PEM format
* @note Could be used in a dialog to confirm the use of the certificate
*/
fun certInfo(cert_PEM: String?): String {
if (cert_PEM == null) {
return "No cert"
}
val cf = CertificateFactory.getInstance("X.509")
val caInput = cert_PEM.byteInputStream()
val ca = caInput.use {
cf.generateCertificate(it)
}
if (ca == null) return "Error" // this shouldn't happen
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(ca.encoded)
val sha256 = digest.joinToString(separator = ":", transform = { "%02x".format(it) })
val subject = (ca as X509Certificate).subjectDN.name
return "Subject:\n$subject\nSHA256:\n$sha256"
}
fun certInfo(res:Pair<String?, Exception?> ) : String {
return certInfo(res.component1())
}
}
}
view raw TlsTest.kt hosted with ❤ by GitHub


P.S. I'm neither a security expert nor a proficient Kotlin programmer. If you spot errors or can suggest improvements, let me know.

Mittwoch, 15. Februar 2017

Playing HLS streams with mpd

If you're playing with the idea to turn your Raspberry Pi into an internet radio you will sooner or later come across the MusicPlayerDaemon (mpd).  It's a server daemon without an user interface with the sole objective to play music and manage playlists.  It exposes however a control port over which command line utilities, apps on mobile phones, or even desktop applications can talk to it using a common “command language”.  This way they can make mpd create playlists which can be stored on the server, play songs from a playlist, stop the playback, skip titles etc.

Even though the content usually resides on your hard disc mpd can fetch it from other devices, e.g. a NAS on your LAN, or even from (web radio stations on) the internet.  The list of your favourite stations boils down to a simple playlist containing their urls, switching stations is the same as skipping to the next song in the playlist of your favourite artists.

Most stations use MP3 or AAC streams which are easily handled by the version of mpd available in the repository of your Linux distribution, even though that version might be a little dated.

HLS streams - as deployed by the BBC for example - are a little trickier.  In order to get to the music various playlists have to be downloaded and parsed, and every 10 seconds or so, the next chunk has to be downloaded from another url.  ffmpeg (or its fork avconv) can handle this overhead but even though ffmpeg is compiled into most version of mpd installed from repositories, trying to open an HLS stream will not work.

The current (Feb. 2017) repository version of mpd is 0.19.1.  You can check the decoder plugins:

$ ./mpd --version
Music Player Daemon 0.19.1
...
Decoders plugins:
...
[ffmpeg] 16sv 3g2 3gp 4xm …


The solution to remedy this situation is already in the source code – perhaps a little hidden. You will have to compile mpd as described below.

However, I did install the version from the repository first. It comes with some system integration like start and stop scripts for the boot process, setting up of an mpd user account on the Raspberry, etc. I've modified the scripts in a few places to point them to the freshly compiled mpd version.

As we have to compile it anyway, let's us the newest version from the mpc homepage.

(The following steps have been tested with mpc 0.20.4 on a Raspberry Pi 3.
In the description below change the version number accordingly.)

Start by installing the required libraries:

sudo apt-get install g++ \
  libmad0-dev libmpg123-dev libid3tag0-dev \
  libflac-dev libvorbis-dev libopus-dev \
  libadplug-dev libaudiofile-dev libsndfile1-dev libfaad-dev \
  libfluidsynth-dev libgme-dev libmikmod2-dev libmodplug-dev \
  libmpcdec-dev libwavpack-dev libwildmidi-dev \
  libsidplay2-dev libsidutils-dev libresid-builder-dev \
  libavcodec-dev libavformat-dev \
  libmp3lame-dev \
  libsamplerate0-dev libsoxr-dev \
  libbz2-dev libcdio-paranoia-dev libiso9660-dev libmms-dev \
  libzzip-dev \
  libcurl4-gnutls-dev libyajl-dev libexpat-dev \
  libasound2-dev libao-dev libjack-jackd2-dev libopenal-dev \
  libpulse-dev libroar-dev libshout3-dev \
  libmpdclient-dev \
  libnfs-dev libsmbclient-dev \
  libupnp-dev \
  libavahi-client-dev \
  libsqlite3-dev \
  libsystemd-daemon-dev libwrap0-dev \
  libcppunit-dev xmlto \
  libboost-dev \
  libicu-dev


Download the source code:

wget https://www.musicpd.org/download/mpd/0.20/mpd-0.20.4.tar.xz

You might want to check the GPG signature:

wget https://www.musicpd.org/download/mpd/0.20/mpd-0.20.4.tar.xz.sig
gpg --verify mpd-0.20.4.tar.xz.sig


Unpack the archive:

tar xvfJ mpd-0.20.4.tar.xz
cd mpd-0.20.4
./configure


Now to the special magic. The ./configure utility has scanned the system environment and has written its findings into configure.h. Open that file with an editor. You should be able to find the following line:

#define ENABLE_FFMPEG 1

This means that the ffmpeg libraries have been detected and will be used.
Now add the following line and save the file:

#define HAVE_FFMPEG 1

This line will change the fallback decoder in src/decoder/DecoderThread.cxx from “mad” to “ffmpeg”. ffmpeg can handle m3u8 playlists typically used by HLS while mad can not.

Now start the build process with

make

and if there are no errors install mpd:

sudo make install


On the Raspberry this new version is stored in /usr/local/bin while the original version still remains in /usr/bin.

Confirm the version of the new file:

$ /usr/local/bin/mpd --version
Music Player Daemon 0.20.4


Additional changes

The following changes of the initial mpd install are necessary to get the new version running on the Raspberry Pi.

Raspberry uses systemd.  There is a control file for the mpd service that needs to be changed:

sudo nano /lib/systemd/system/mpd.service

change:
ExecStart=/usr/bin/mpd --no-daemon $MPDCONF
to the new location:
ExecStart=/usr/local/bin/mpd --no-daemon $MPDCONF

Keep in mind that this change might be overwritten if the repository version of mpd is being updated later on... which doesn't happen that often.

Uncomment the following line in /etc/default/mpd. This will define the variable MPDCONF.

MPDCONF=/etc/mpd.conf


Let us change some settings in mpd configuration file /etc/mpd.conf

sudo nano /etc/mpd.conf

In the default configuration mpd and its client must run on the same machine. In order to allow access via the network change:

bind_to_address         "localhost"
to
bind_to_address         "any"

For convenience I've changed my music_directory to a place where I can more easily add music files. Keep in mind that his folder needs to be world readable so that mpd running as user “mpd” can access it.

music_directory         "/home/pi/Music"

Now we have to tell the system to read the new configuration and restart the mpd service.

sudo systemctl daemon-reload
sudo service mpd restart


Check the status of the service:

sudo service mpd status

Unrelated problem

In my first attempts mpd froze after playing the first title.  Someone suggested to remove pulseaudio... and it worked.

sudo apt-get remove pulseaudio
sudo reboot


Links

  • https://www.musicpd.org/doc/user/install_source.html
  • https://www.musicpd.org/download.html
  • https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units