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

WinSSL sends client certificate automatically #2262

Closed
jeroen opened this issue Jan 25, 2018 · 15 comments
Closed

WinSSL sends client certificate automatically #2262

jeroen opened this issue Jan 25, 2018 · 15 comments

Comments

@jeroen
Copy link
Contributor

jeroen commented Jan 25, 2018

The following issue was reported in the repo for the libcurl bindings for the R programming language.

Earlier this year we switched the R curl package from openssl to winssl on windows. Several Windows users have since then complained about getting an error schannel: next InitializeSecurityContext failed for certain servers (usually intranet).

The issue is difficult to reproduce but it seems to be caused by libcurl using an inappropriate client certificate when connecting over https to a server that has not requested a client certificate at all. Most httpd servers will simply ignore the client cert, but some servers (such as vault) will actually refuse the connection if they fail to validate the (unneeded) client cert.

@jay jay changed the title WinSSL uses incorrect client cert - schannel: next InitializeSecurityContext failed WinSSL sends client certificate automatically Jan 25, 2018
@jay jay added the TLS label Jan 25, 2018
@jay
Copy link
Member

jay commented Jan 25, 2018

That thread is two issues which as far as I can tell are unrelated.

The first issue is schannel (aka WinSSL) returning SEC_E_INVALID_TOKEN. The reporter in that thread failed to follow up in that case so I'm ignoring it. Also as I mentioned in the thread I had a similar issue that I think was a bug in schannel, as it was reproducible without libcurl.


The second issue is, to sum it up, that the reporter's server requests a client certificate and schannel then automatically locates and responds with a certificate, and then the server replies with fatal alert TLS1_ALERT_BAD_CERTIFICATE, aka SEC_E_CERT_UNKNOWN. (You can read the full map of alerts -> schannel errors here).

It is default behavior that schannel chooses the client cert to send automatically:

"When the server requests client authentication, the client must send the server one of its certificates. By default, Schannel will, with no notification to the client, attempt to locate a client certificate and send it to the server. To disable this feature, clients specify ISC_REQ_USE_SUPPLIED_CREDS when calling the InitializeSecurityContext (Schannel) function. When this flag is specified, Schannel will return SEC_I_INCOMPLETE_CREDENTIALS to the client when the server requests authentication and the client has not previously supplied a certificate."

We already use ISC_REQ_USE_SUPPLIED_CREDS but only if the server returns incomplete credentials:

curl/lib/vtls/schannel.c

Lines 672 to 681 in d6c21c8

/* If the server has requested a client certificate, attempt to continue
the handshake without one. This will allow connections to servers which
request a client certificate but do not require it. */
if(sspi_status == SEC_I_INCOMPLETE_CREDENTIALS &&
!(BACKEND->req_flags & ISC_REQ_USE_SUPPLIED_CREDS)) {
BACKEND->req_flags |= ISC_REQ_USE_SUPPLIED_CREDS;
connssl->connecting_state = ssl_connect_2_writing;
infof(data, "schannel: a client certificate has been requested\n");
return CURLE_OK;
}

Also of note here is their assumption that the client must send one of its certificates. That may or may not be true, someone needs to check the RFC. I've seen replies with certificate count 0 so maybe it must send it only if one is available?

Anyway, in this case one client certificate is available so that is sent to the server in response to its certificate request. The certificate sent doesn't seem to have anything to do with the server, according to one of the reporters in that thread @weshinsley.

We may be able to offer a way to disable this behavior for example a CURLOPT_SSL_OPTIONS flag like CURLSSLOPT_NO_DEFAULT_CREDS that if on we use to set flag ISC_REQ_USE_SUPPLIED_CREDS at some point before schannel is notified of the client certificate request.

I'm not sure if we are responsible here. If the server (vault - being used as an intranet server by the reporter) rejects an unknown client certificate fatally even if one is not required then one could argue that's the server's fault.

/cc @richfitz @weshinsley

@richfitz
Copy link

Would logs from the server help identify if where the fault lies?

@jeroen
Copy link
Contributor Author

jeroen commented Jan 25, 2018

@jay I agree with one could argue that's the server's fault, but the problem does not seem to appear for any browser (including IE) or any other libcurl SSL back-end on the same machine. So it would be nice if the default behavior were a bit more robust against picky servers.

Perhaps in case of SEC_E_CERT_UNKNOWN curl could attempt a retry without the client cert?

@jay
Copy link
Member

jay commented Jan 25, 2018

I took a look at RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2.

Section 7.4.4 says if the CA list provided by the server is empty (which is what's happening) then the client MAY send any certificate of the appropriate type (which is what's happening).

   certificate_authorities
      A list of the distinguished names [X501] of acceptable
      certificate_authorities, represented in DER-encoded format.  These
      distinguished names may specify a desired distinguished name for a
      root CA or for a subordinate CA; thus, this message can be used to
      describe known roots as well as a desired authorization space.  If
      the certificate_authorities list is empty, then the client MAY
      send any certificate of the appropriate ClientCertificateType,
      unless there is some external arrangement to the contrary.

So we can add an "external arrangement", ie the flag NO_DEFAULT_CREDS.

Section 7.4.6 allows the server to send a fatal alert or continue the handshake in response to the client's certificate message.

7.4.6.  Client Certificate

   When this message will be sent:

      This is the first message the client can send after receiving a
      ServerHelloDone message.  This message is only sent if the server
      requests a certificate.  If no suitable certificate is available,
      the client MUST send a certificate message containing no
      certificates.  That is, the certificate_list structure has a
      length of zero.  If the client does not send any certificates, the
      server MAY at its discretion either continue the handshake without
      client authentication, or respond with a fatal handshake_failure
      alert.  Also, if some aspect of the certificate chain was
      unacceptable (e.g., it was not signed by a known, trusted CA), the
      server MAY at its discretion either continue the handshake
      (considering the client unauthenticated) or send a fatal alert.

So we have a server that is asking for any certificate and then fatally terminating the handshake by saying the certificate is bad... I'm thinking the server should be at fault here.


Would logs from the server help identify if where the fault lies?

You could check and see if you can spot where the certificate is rejected. And then maybe you can walk that back to something in the configuration. I don't suggest posting the entire log here because it may contain sensitive information.

@jay I agree with one could argue that's the server's fault, but the problem does not seem to appear for any browser (including IE) or any other libcurl SSL back-end on the same machine. So it would be nice if the default behavior were a bit more robust against picky servers.

Perhaps in case of SEC_E_CERT_UNKNOWN curl could attempt a retry without the client cert?

Based on the info we have right now I'm hesitant to support this. IE is probably over compensating for server problems. We already have code to continue the connection, and that could probably be improved for other alerts such as unknown certificate. But in this case it's a fatal error. I'm just not convinced that we should retry on fatal error.

@jeroen
Copy link
Contributor Author

jeroen commented Jan 26, 2018

Thanks for looking up the appropriate RFC sections.

So we have a server that is asking for any certificate and then fatally terminating the handshake by saying the certificate is bad... I'm thinking the server should be at fault here.

Hmm the way I read it is that the server only lets the client know it MAY authenticate, if it wishes to identify itself. I don't think the intention here is to send a random cert.

The current default behavior is actually a privacy concern. I do not expect curl to reveal my personal identity to any random server by default. For example in some countries you need to install a client cert to do your taxes online, but I certainly do not want this cert to be used for any other website.

I don't understand how the user should set CURLOPT_SSLCERT with WinSSL. The documentation says:

With NSS or Secure Transport, this can also be the nickname of the certificate you wish to authenticate with as it is named in the security database.

Is there something similar for Windows to identify a client cert from the certificate store?

@richfitz
Copy link

This is the log from a failed request:

2018/01/26 12:30:31 http: TLS handshake error from 129.31.24.107:49835: tls: failed to parse client certificate: asn1: syntax error: PrintableString contains invalid character

@jay
Copy link
Member

jay commented Jan 26, 2018

Hmm the way I read it is that the server only lets the client know it MAY authenticate, if it wishes to identify itself. I don't think the intention here is to send a random cert.

Whose intention. With an empty CA list given by the server the client MAY send any certificate for authentication in reply and that is what's happening:

"By default, Schannel will, with no notification to the client, attempt to locate a client certificate and send it to the server."

So I think the schannel intention is clear. The issue here is should we defer to that. Obviously the MS wording and the RFC differ in that the MS says the client must reply with one of its certificates whereas the RFC says the client must reply to the certificate request but that reply can be 0 certificates. If a cert is sent but the server doesn't recognize it then the server can continue without client auth or respond with a fatal alert which is why I think the onus is on the server.

The current default behavior is actually a privacy concern. I do not expect curl to reveal my personal identity to any random server by default. For example in some countries you need to install a client cert to do your taxes online, but I certainly do not want this cert to be used for any other website.

Yes I agree but I also think it would be a breaking change now if we override the schannel default behavior. I'm not 100% against it I just think it's going to break transfers of people who have come to expect that the same way they do in IE.

I don't understand how the user should set CURLOPT_SSLCERT with WinSSL.

There is no setting right now, if there's a cert it's sent automatically.


failed to parse client certificate: asn1: syntax error: PrintableString contains invalid character

wes.cer.zip

openssl asn1parse -in wes.cer | grep -B1 PRINTABLESTRING
   46:d=5  hl=2 l=   3 prim: OBJECT            :commonName
   51:d=5  hl=2 l=  21 prim: PRINTABLESTRING   :Communications Server
--
  112:d=5  hl=2 l=   3 prim: OBJECT            :commonName
  117:d=5  hl=2 l=  24 prim: PRINTABLESTRING   :w.hinsley@imperial.ac.uk

@ is not valid in PRINTABLESTRING so technically they're correct the string is invalid. As noted in that wikipedia and elsewhere on the web e-mails are put in commonName even though that is not correct.

@weshinsley
Copy link

weshinsley commented Jan 26, 2018

In case it's useful, I did some more testing today. I requested and got a new personal client certificate from Imperial - this one I'll call the GOOD certificate - it doesn't have @ in the PRINTABLESTRING, and the certificate path reports OK as far as the Imperial College Root. It doesn't have anything specifically to do with support.montagu.dide.ic.ac.uk:8200 - but the root certificate authority is .ic.ac.uk. I'll call my old Lyncpool cert the "BAD" one, because of problems with both those things. (If I only I had an UGLY certificate too...)

Testing takes some care, as after a successful connection, some caching happens somewhere and I seem to get success no matter what I change. I found a local reboot cleared that. Also, some drag and drop procedures in certmgr.msc seem to not take effect until you push F5 to refresh, even when the result of the drag-drop is visible on screen. That one confused me a few times. So in the end, for each test, I set up the certificates, refresh, reboot, and do the R call... The variable is: which certificates are in my "Personal Certificates" folder, on my Win 10 desktop. Four tests:

BAD certificate only: SEC_E_CERT_UNKNOWN
GOOD certificate only: SUCCESS
No certificates at all: SUCCESS
GOOD & BAD certificate - SEC_E_CERT_UNKNOWN

This last one: does this means that all (well, both!) my client certs get sent to the server, and then a fatal error gets thrown if only one of them is bad?

Edit - I tried the GOOD & BAD a few times to try and see if it was picking just one to send, but got consistent SEC_E_CERT_UNKNOWN. There's no "ordering" as far as I can tell in certmgr.msc - F5 sorts alphabetically. I also tried chronologically both orders to see if "most recently added"... but no difference. So it's either both get sent and it fails, or it consistently chooses to send my BAD certificate, via a criteria I don't know. (Or I've just been very unlucky each time)

@jay
Copy link
Member

jay commented Jan 26, 2018

GOOD & BAD certificate - SEC_E_CERT_UNKNOWN

This last one: does this means that all (well, both!) my client certs get sent to the server, and then a fatal error gets thrown if only one of them is bad?

Edit - I tried the GOOD & BAD a few times to try and see if it was picking just one to send, but got consistent SEC_E_CERT_UNKNOWN. There's no "ordering" as far as I can tell in certmgr.msc - F5 sorts alphabetically. I also tried chronologically both orders to see if "most recently added"... but no difference. So it's either both get sent and it fails, or it consistently chooses to send my BAD certificate, via a criteria I don't know. (Or I've just been very unlucky each time)

It can send any certificate since the server is not requesting a specific CA. Run Wireshark and look at the certificate details to see which certificate(s) it's sending. Check the certificate that is sent, for example from your older wireshark capture in the other thread:

<- Certificate, Server Key Exchange, Certificate Request, ...
-> Certificate, Client Key Exchange, Certificate Verify, ... (check this)

capture

@weshinsley
Copy link

Right - it's just sending one certificate each time - but it's always the bad one!

@jay
Copy link
Member

jay commented Jan 26, 2018

Ok. We can assume MS doesn't think of the cert as bad if it's sending it. Though surely we would like to be able to stop it from doing that, but short of disabling auto credentials I don't know what else we can do. I think the way forward is one of:

  • Add a flag to disable the auto-credential behavior, or:
  • Disable by default the auto-credential behavior and add a flag to enable it.

As far as I know this is only an issue with WinSSL, no other backend is automatically choosing client certs is it?

@jeroen
Copy link
Contributor Author

jeroen commented Jan 27, 2018

If we apply the principle of least surprise, I would expect the behavior to be as much as possible consistent with other libcurl tls back-ends. So that means disabling the auto credential behavior and making it opt-in via CURLOPT_SSLCERT somehow.

If the Schannel API does not provide a mechanism to use a particular cert (which I find very odd) perhaps it can support CURLOPT_SSLCERT = "auto" to enable to automatic client cert?

@bathyg
Copy link

bathyg commented Apr 2, 2018

Here is a webpage that can be opened by R on linux/OSX (openssl) but not windows (winssl) "https://edoras.sdsu.edu/~jjfan/stat678/pbcWH.txt"
Anything using curl, will return SEC_E_INVALID_TOKEN on Windows 10, R 3.4.4

@jeroen
Copy link
Contributor Author

jeroen commented Apr 2, 2018

@bathyg it seems to work fine here on windows. Do you have any custom certs installed in your windows certificate manager?

@jay jay closed this as completed in 3de6074 Jan 29, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Apr 29, 2019
jay added a commit to jay/curl that referenced this issue Feb 27, 2021
- New libcurl ssl option value CURLSSLOPT_NO_DEFAULT_CREDS tells libcurl
  to not automatically locate and use a client certificate for
  authentication.

- New curl tool options --ssl-no-default-creds
  and --proxy-ssl-no-default-creds map to CURLSSLOPT_NO_DEFAULT_CREDS.

This option is only supported for Schannel (the native Windows SSL
library). By default, Schannel will, with no notification to the client,
attempt to locate a client certificate and send it to the server (when
requested by the server). That could be considered a privacy violation
and unexpected.

Fixes curl#2262
Reported-by: Jeroen Ooms
Assisted-by: Wes Hinsley
Assisted-by: Rich FitzJohn

Ref: https://curl.se/mail/lib-2021-02/0066.html
Reported-by: Morten Minde Neergaard

Closes #xxxx
jay added a commit to jay/curl that referenced this issue Feb 27, 2021
- Disable auto credentials by default. This is a breaking change
  for clients that are using it, wittingly or not.

- New libcurl ssl option value CURLSSLOPT_AUTO_CREDS tells libcurl
  to automatically locate and use a client certificate for
  authentication, when requested by the server.

- New curl tool options --ssl-auto-creds and --proxy-ssl-auto-creds map
  to CURLSSLOPT_AUTO_CREDS.

This option is only supported for Schannel (the native Windows SSL
library). Prior to this change Schannel would, with no notification to
the client, attempt to locate a client certificate and send it to the
server, when requested by the server. Since the server can request any
certificate that supports client authentication in the OS certificate
store it could be a privacy violation and unexpected.

Fixes curl#2262
Reported-by: Jeroen Ooms
Assisted-by: Wes Hinsley
Assisted-by: Rich FitzJohn

Ref: https://curl.se/mail/lib-2021-02/0066.html
Reported-by: Morten Minde Neergaard

Closes #xxxx
@jay
Copy link
Member

jay commented Feb 27, 2021

To address this issue I've created two PRs.

#6672 would leave auto credentials as the default and add an option to disable it
#6673 would disable auto credentials as the default (breaking change) and add an option to enable it

I'm leaning towards #6673, please take feedback there.


Edit: To work on this issue I used an nginx server with option ssl_verify_client optional_no_ca; and a client certificate with enhanced key usage for client authentication (attached) that I created in Windows 10 like this:

New-SelfSignedCertificate -Type Custom -DnsName test -KeySpec Signature `
-Subject "CN=test" -KeyExportPolicy Exportable `
-HashAlgorithm sha256 -KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" `
-NotAfter (Get-Date).AddYears(100) `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2")

test-cert-with-client-auth.zip can be imported into the Personal Certificate store on Windows 7+.

jay added a commit that referenced this issue Apr 22, 2021
- Disable auto credentials by default. This is a breaking change
  for clients that are using it, wittingly or not.

- New libcurl ssl option value CURLSSLOPT_AUTO_CLIENT_CERT tells libcurl
  to automatically locate and use a client certificate for
  authentication, when requested by the server.

- New curl tool options --ssl-auto-client-cert and
  --proxy-ssl-auto-client-cert map to CURLSSLOPT_AUTO_CLIENT_CERT.

This option is only supported for Schannel (the native Windows SSL
library). Prior to this change Schannel would, with no notification to
the client, attempt to locate a client certificate and send it to the
server, when requested by the server. Since the server can request any
certificate that supports client authentication in the OS certificate
store it could be a privacy violation and unexpected.

Fixes #2262
Reported-by: Jeroen Ooms
Assisted-by: Wes Hinsley
Assisted-by: Rich FitzJohn

Ref: https://curl.se/mail/lib-2021-02/0066.html
Reported-by: Morten Minde Neergaard

Closes #6673
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
6 participants