2

I am trying to use libcurl with public-key pinning in order to verify a server's authenticity when downloading a file.

Curl is compiled so that it doesn't use any certificates on the system, but only relies on certificates it receives from the user:

./configure --without-ca-bundle --without-ca-path --without-ca-fallback && make

First I obtain the sha256 sum of the server certificate's public key, as explained here:

$ openssl s_client -servername www.example.com -connect www.example.com:443 < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > www.example.com.pem
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 High Assurance Server CA
verify return:1
depth=0 C = US, ST = California, L = Los Angeles, O = Internet Corporation for Assigned Names and Numbers, OU = Technology, CN = www.example.org
verify return:1
DONE
$ openssl x509 -in www.example.com.pem -pubkey -noout > www.example.com.pubkey.pem
$ openssl asn1parse -noout -inform pem -in www.example.com.pubkey.pem -out www.example.com.pubkey.der
$ openssl dgst -sha256 -binary www.example.com.pubkey.der | openssl base64
xmvvalwaPni4IBbhPzFPPMX6JbHlKqua257FmJsWWto=

Then I set the public key's hash and other related options in libcurl:

curl_easy_setopt(conn, CURLOPT_PINNEDPUBLICKEY, "sha256//xmvvalwaPni4IBbhPzFPPMX6JbHlKqua257FmJsWWto=");
curl_easy_setopt(conn, CURLOPT_SSL_VERIFYPEER, 1);
curl_easy_setopt(conn, CURLOPT_SSL_VERIFYHOST, 2);
curl_easy_setopt(conn, CURLOPT_URL, "https://example.com/index.html");
curl_easy_setopt(conn, CURLOPT_VERBOSE, 1);
curl_code = curl_easy_perform(conn);
if (curl_code != CURLE_OK)
{
    printf("%s\n", curl_easy_strerror(curl_code));
}

The download fails with an error:

* SSL certificate problem: unable to get local issuer certificate
...
Peer certificate cannot be authenticated with given CA certificates

Well, it seems curl is looking for some certificates, so I recompile it in order for it to include the default certificates:

./configure && make

Now, the download will work:

* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
...
*  SSL certificate verify ok.
*    public key hash: sha256//xmvvalwaPni4IBbhPzFPPMX6JbHlKqua257FmJsWWto=
...

In the CURLOPT_PINNEDPUBLICKEY documentation, it is explained:

When negotiating a TLS or SSL connection, the server sends a certificate
indicating its identity. A public key is extracted from this certificate
and if it does not exactly match the public key provided to this option,
curl will abort the connection before sending or receiving any data. 

So my impression was that curl only needs the public key from the user, in order to compare it with the public key extracted from the server's certificate.

What am I missing here?

Claudiu
  • 2,124
  • 4
  • 26
  • 36
  • Please note that you should no longer use public key pinning, because it has been dropped by google. For example see here: https://www.zdnet.com/article/google-chrome-is-backing-away-from-public-key-pinning-and-heres-why/ – PowerStat Aug 31 '18 at 12:27
  • @PowerStat That is about HPKP, which from what I understand is servers using HTTP headers to tell clients which public keys to pin to; so Chrome no longer responds to those headers sent by servers. But the technique itself of public key pinning is still valid and probably more secure than certificate pinning. – Claudiu Aug 31 '18 at 12:58
  • For the user who down-voted the question - I would also appreciate your feedback so I can improve this and future questions. – Claudiu Sep 04 '18 at 20:11
  • The problem with pinning is that you have to renew the pins before (depending on your refresh interval) you update a key/cert. This could become a problem when you think about Let's encrypt and renewing your public keys/certs every 2 month - please keep that in mind. Because of the shorter renewable time periods pinning could become evil, so that a website is not accessible for some days/weeks/months! – PowerStat Sep 06 '18 at 12:20
  • @PowerStat Of course, it is the responsability of the application which is doing the pinning to refresh the pins often. Curl for example allows setting multiple public keys at once; so when your current certificate is close to expire you issue the new one and you pass both public keys to curl. When the new certificate comes into effect it will fail on the previous pin but work on the new one. So yes it is more problematic but it's certainly doable. – Claudiu Sep 06 '18 at 12:30
  • You will be in trouble when your certificate will be revoked - think about the symantec certificate trouble with google. So what I am doing in my clients is to check the server certificate and if it is different then I am doing some more checks to see if it was a normal renew of the cert - and if not a human has to review the cert and decide to accept it or not. Thats like warning about a man-in-the-middle-attack. Pinning has to many negative side effects - that is why google and many others have removed it. – PowerStat Sep 06 '18 at 12:41

1 Answers1

1

The problem is that CURLOPT_SSL_VERIFYPEER being set to 1 enables CA pinning. Curl accepts setting both CA pinning and public-key pinning at the same time, and because CA pinning is tried before public key pinning, the CA pinning fails and it never gets to do the public key pinning.

The solution is to explicitly disable CA pinning when doing public key pinning:

curl_easy_setopt(conn, CURLOPT_SSL_VERIFYPEER, 0);

This needs to be done explicitly because the default value for CURLOPT_SSL_VERIFYPEER is 1.

NOTE: setting CURLOPT_SSL_VERIFYPEER to 0 should generally be avoided, but in this case it is safe because public-key pinning is being done.

For more details also see this curl issue.

Claudiu
  • 2,124
  • 4
  • 26
  • 36