Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public-key pinning doesn't work when compiling with '--without-ca-bundle --without-ca-path' #2935

Closed
claudiusaiz opened this issue Sep 3, 2018 · 6 comments
Labels

Comments

@claudiusaiz
Copy link

claudiusaiz commented Sep 3, 2018

I did this

I compiled curl the following way:
./configure --enable-debug --disable-optimize --enable-curldebug --without-ca-bundle --without-ca-path && make

I obtained the sha256 sum of the server's certificate public key exactly as written here. Then I wrote a small program to try to use public-key pinning:

curl_easy_setopt(conn, CURLOPT_FAILONERROR, 1);
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);
curlCode = curl_easy_perform(conn);
if (curlCode != CURLE_OK)
{
    printf("curl_easy_perform failed: %s\n", curl_easy_strerror(curlCode));
}

The output is:

* STATE: INIT => CONNECT handle 0x55c6fa7d3d68; line 1397 (connection #-5000)
* Added connection 0. The cache now contains 1 members
*   Trying 93.184.216.34...
* TCP_NODELAY set
* STATE: CONNECT => WAITCONNECT handle 0x55c6fa7d3d68; line 1450 (connection #0)
* Connected to example.com (93.184.216.34) port 443 (#0)
* STATE: WAITCONNECT => SENDPROTOCONNECT handle 0x55c6fa7d3d68; line 1557 (connection #0)
* Marked for [keep alive]: HTTP default
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* STATE: SENDPROTOCONNECT => PROTOCONNECT handle 0x55c6fa7d3d68; line 1571 (connection #0)
* SSL certificate problem: unable to get local issuer certificate
* Marked for [closure]: Failed HTTPS connection
* multi_done
* Curl_http_done: called premature == 1
* stopped the pause stream!
* Closing connection 0
* The cache now contains 0 members
* Expire cleared
curl_easy_perform failed: Peer certificate cannot be authenticated with given CA certificates

I expected the following

The download should work. When building curl without the "--without-ca-bundle --without-ca-path" flags, the download works.
I first observed this behaviour on a system which had no certificates installed on it, so I used '--without-ca-bundle --without-ca-path' just so that this problem can be more easily reproduced. This can be reproduced with the curl command-line tool as well, if built with the "without-*" flags.

curl/libcurl version

7.61.0

operating system

$ cat /etc/issue 
Arch Linux \r (\l)
$ uname -srvmo
Linux 4.15.13-1-ARCH #1 SMP PREEMPT Sun Mar 25 11:27:57 UTC 2018 x86_64 GNU/Linux
@bagder bagder added the TLS label Sep 3, 2018
@bagder
Copy link
Member

bagder commented Sep 3, 2018

@moparisthebest you up to check what this is about?

@claudiusaiz
Copy link
Author

Debugging through the curl and openssl code, it seems to me that even if public-key pinning is the selected option, CA pinning is also being done, and this is why the failure occurs.
The logic starts from ossl_connect_common():

  while(ssl_connect_2 == connssl->connecting_state ||
        ssl_connect_2_reading == connssl->connecting_state ||
        ssl_connect_2_writing == connssl->connecting_state) {

    ...
    /* Run transaction, and return to the caller if it failed or if this
     * connection is done nonblocking and this loop would execute again. This
     * permits the owner of a multi handle to abort a connection attempt
     * before step2 has completed while ensuring that a client using select()
     * or epoll() will always have a valid fdset to wait on.
     */
    result = ossl_connect_step2(conn, sockindex); // -------> the certificates are verified here
    if(result || (nonblocking &&
                  (ssl_connect_2 == connssl->connecting_state ||
                   ssl_connect_2_reading == connssl->connecting_state ||
                   ssl_connect_2_writing == connssl->connecting_state)))
      return result;

  } /* repeat step2 until all transactions are done. */

  if(ssl_connect_3 == connssl->connecting_state) {
    result = ossl_connect_step3(conn, sockindex); // --------> the public key is checked here
    if(result)
      return result;
  }

ossl_connect_step2() calls SSL_connect() from openssl, which at some point verifies the certificate chain in the X509_verify_cert() function:

/* We now lookup certs from the certificate store */
for (;;) {
    /* If we have enough, we break */
    if (depth < num)
        break;
    /* If we are self signed, we break */
    if (cert_self_signed(x))
        break;
    ok = ctx->get_issuer(&xtmp, ctx, x); // -----> can't find the issuer of the intermediate certificate, so fails with ok = 0

    if (ok < 0) {
        ctx->error = X509_V_ERR_STORE_LOOKUP;
        goto err;
    }
    if (ok == 0)
        break;
    x = xtmp;
    if (!sk_X509_push(ctx->chain, x)) {
        X509_free(xtmp);
        X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE);
        ctx->error = X509_V_ERR_OUT_OF_MEM;
        ok = -1;
        goto err;
    }
...

So because the ossl_connect_step2() fails it never ends up calling ossl_connect_step3(), where the public key is actually being checked.

@bagder
Copy link
Member

bagder commented Sep 4, 2018

I figure you need to explicitly switch off the CA verification with CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST.

@claudiusaiz
Copy link
Author

Thank you, it works this way. It now says:
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
, and then it continues with the download.

Actually only disabling CURLOPT_SSL_VERIFYPEER seems to be necessary; I guess the host name can still be checked against the one from the received certificate.

But is this safe? In the documentation for CURLOPT_SSL_VERIFYPEER it says:
WARNING: disabling verification of the certificate allows bad guys to man-in-the-middle the communication without you knowing it. Disabling verification makes the communication insecure. Just having encryption on a transfer is not enough as you cannot be sure that you are communicating with the correct end-point.
I'm guessing this warning doesn't apply in this case because even if an attacker would forge a certificate it would still need to have a public key with the exact same SHA256 hash as the one used by the client...

@bagder
Copy link
Member

bagder commented Sep 4, 2018

But is this safe?

The documentation for that option doesn't really take pinning into consideration. Key pinning is really an alternative to the regular VERIFYPEER so yes, I would qualify that as still safe. We should probably attempt to phrase that a little better in the documentation.

And as you say, the VERIFYHOST verifies that the host is correct in the server cert so you can probably keep that as it really should match even when you use pinning.

@claudiusaiz
Copy link
Author

Thanks for the confirmation. That makes it very clear.

@lock lock bot locked as resolved and limited conversation to collaborators Dec 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Development

No branches or pull requests

2 participants