Clarification on CORS preflight fetches for TLS client certificates
TLS client certificates are included within CORS notion of credentials, for the reasons captured in CORS protocol and credentials
When considering cookies, it's sufficient to omit cookies from the request. When considering HTTP auth, an attempt to request authentication (e.g. a 403 response) will be treated as a non-successful response status, and the CORS preflight will fail.
However, when considering TLS client certificates, there exists ambiguity in the processing model and expectation of the preflight. When the server/peer requests a client certificate, the client performing the CORS check has two possible options available to them:
- Treat the server's CertificateRequest as equivalent to a 403 and fail the request
- Treat the server's CertificateRequest as (possibly) optional, and attempt to continue the handshake without sending the peer a certificate, continuing the preflight check.
There are pros and cons to both methods, but the most notable one, which prompted this bug, is ambiguity in how server operators should configure mutual TLS authentication.
Generally, HTTP servers tend to offer at least three modes for client certificates - not configured, request (but don't require), or require. The difference in these latter two modes is that if it's set to "request (but don't require)", if the peer declines to send a certificate, that's generally passed up to the application layer to determine - for example, it might fail the request with an error code to indicate the user is not authenticated. However, the TLS session will have been considered successful, entered into TLS session caches, etc.
Alternatively, if they're set to "require", the server will fail the TLS handshake, preventing it from being added to any TLS session cache or otherwise reaching the application layer.
There's also a fourth mode, which is not valid in HTTP/2, which is "renegotiate". In this scenario, the request for a client certificate is not made during the initial TLS handshake. Instead, the server allows the peer to request the resource, and based on those request parameters, may have the application layer signal to the server layer that a TLS client certificate is expected. The server then sends a response to the client to initiate renegotiation, and during that renegotiation handshake, a client certificate is then requested and provided. This is banned from H2 and subsequent, due to the multiplexing at the application layer being incompatible with request-level authorization over the transport.
If the decision is that the CORS request should be aborted upon request for certificate, then it means that a server cannot use the request or require directives (and thus, not use H2), because any attempt to request a certificate during the initial handshake will cause CORS preflights to fail.
If the decision is that the CORS request should attempt to continue the handshake, without authentication, then user agents need to ensure that their connection pool is appropriately segmented, as the deliberate omission of the certificate may prevent the server from prompting, and the TLS session cache, if used, may prevent future attempts to authenticate.
In trying to determine how best to resolve the Chromium spec-compliance bug, we noted that it's ambiguous as to what the expectation is. Our preference, which has considered feedback from Enterprises (including Google) that make heavy use of client certificates to protect and authenticate internal resources, is to continue the TLS handshake without sending a client certificate.
However, that does mean that server operators deploying such technologies need to be aware that even though they are deploying client certificates to protect the resources, an improperly implemented CORS preflight may end up disclosing that there exist resources only visible to authenticated users.
Knowing that this area of Chromium's lack of spec compat has been a source of concern for Mozilla and Microsoft in the past, it would be great to know what they feel is appropriate, and great to capture this in the spec.
@sideshowbarker @annevk @davidben to make sure I haven't botched any explanations and for thoughts :)
As the bug reporter for https://bugs.chromium.org/p/chromium/issues/detail?id=775438, I think the explanation is great and aligns spot-on with the actual real-world problem cases I'm aware of where people have run into this in the wild (e.g., frustrated web developers posting to Stack Overflow who are baffled by the difference in browser behavior on this between Firefox and Chrome.
So I agree we should update the spec such that we can have all implementations aligned on this ー to have agreement among implementors on the processing requirements, and spec text which leaves no doubts about what those requirements are.
So, thanks much for taking time to raise this.
I was asked to comment on Firefox's implementation. If I'm reading it correctly, when making CORS preflight fetches, we set a flag[1] that eventually makes its way to where we configure the handshake to not send a client certificate[2].
[1] https://hg.mozilla.org/mozilla-central/file/6f3709b38781/netwerk/protocol/http/nsCORSListenerProxy.cpp#l1462 [2] https://hg.mozilla.org/mozilla-central/file/6f3709b38781/security/manager/ssl/nsNSSIOLayer.cpp#l2278
@mozkeeler Thanks! I was initially confused by a misleading comment in the NSS code, but yeah, it does look like for TLS 1.0+, it sends an empty cert
Re TLS 1.0+ vs SSL 3.0, that's just an encoding quirk. In SSL 3.0, affirmatively sending no certificate was denoted by omitting the Certificate message and sending a no_certificate warning alert. In TLS 1.0+, if the client receives a CertificateRequest, it must send a Certificate message, and sending no certificate is denoted by sending an empty Certificate.
The two options are functionally equivalent. Just the spelling was changed. (Not that matters now as SSL 3.0 is gone.)
Firefox now has a Chrome-compatible mode that's disabled by default. But we might consider changing that if we keep running into issues. (Tracked by https://bugzilla.mozilla.org/show_bug.cgi?id=1019603.)
I'd be curious to hear from @youennf and others from Safari with regards to what their thinking is on this.
This is in reply to @sleevi in the other issue (https://github.com/whatwg/fetch/issues/1181: Promp for/send TLS client certificates for CORS preflights):
The Chrome side is most definitely a bug, and while organizational changes have inhibited us from fixing, we do see it as a bug that can and should be fixed.
I'm proposing to change the spec so that client certificates would be sent during a preflight (and then it won't be a bug in Chrome). But what I think is most important is will this change make security worse? (because this is the reason why credentials are not sent in preflights in the first place).
I think not because the same client certificate promp is possible to trigger using a non-preflight requiring fetch, or even with an <img src="https://cert-requiring-other-origin.com/image.png">
Prompting for sub-resources is equally an anti-pattern, in that it's confusing to end users to understand what, why, and how. Browsers have been moving away, particularly for subresources, from prompting the end user for credentials.
UX-wise, using client certificates is a burden indeed (the prompt can't be designed, cert install is manual, you won't be prompted again if certificates are optional and you chose not to send one in the beginning, but then want it to be sent one in tho future to the same host, etc. Painful).
The up-side is that it's based on public key crypto and known to be secure, and also, web content is known not to have any access to these (unlike cookies, HTTP authentication headers etc).
The premise that this information is OK to leak is also flawed, because such information very much should be considered sensitive, both in terms of identifying the user to potential network adversaries, and to the end-server.
What I meant to say was that in public key cryptography certificates are public, so when someone is eavesdropping and saving the certificate then it still won't make it possible for the attacker to authenticate with it.
The problem stems from the fact that, at the core, client certificates are poorly designed and violate a number of good security practices. At a minimum, using optional is the expectation, but as you note, even then, it can be problematic. Proposals have been made (e.g. Secondary Certificates, CATCH) to move this up to the application layer, which is really where they belong.
I think there's a core takeaway here: mTLS is, and has always been, a giant hack. Literally, its introduction in SSL2 was merely a "we might want this", not with any fundamental design considerations, and its interactions with HTTP have never been well-considered. Just like stream-oriented authentication (e.g. NTLM, Kerberos) has been explicitly forbidden with HTTP/2, mTLS is, at least within the interaction of a broader HTTP stack, a problematic anti-pattern. If you are going to deploy it, as you note, the solution is that your application layer needs to be ready and aware that the client may decline to send credentials, and handle authentication at the request layer (as with every other HTTP authentication method), not at the transport layer.
I don't agree with the above at all. Client certificates are a way to authenticate the client to the server, very much like how server certificates authenticate the server to the client.
If you mean the UX isn't great, then absolutely, it is not. But the choice to accept this inconvenience should be up to the implementer. Sending client certificates in the preflight will make using client certificates slightly better in this regard. :) Are there any downsides?
But what I think is most important is will this change make security worse?
Yes. It deliberately introduces ambient auth for a channel/transport based auth method, which we have two decades of experience from NTLM/Kerberos to know this causes a host of security issues.
If you mean the UX isn't great, then absolutely, it is not.
No, I mean at a fundamental technology level. Every aspect of the stack, from issuance, verification, and consumption within an application is layers upon layers of hacks, in which any and every mistake is a critical security issue. I understand that the appeal with mTLS is “it’s not a bearer token”, but please understand that in its current state, it’s not a great tech for achieving that. However, beyond just the “mTLS is designed to be failure prone”, which is more about the design and implementation choices, it was literally designed around a premise that a browser would only make a single HTTP/1.0 connection to the server, request a resource, and disconnect. The idea of browsers making multiple connections to fetch resources was, even in its time, seen to break the security assumptions of mTLS, just as multiple requests (HTTP/1.1) does. In today’s modern world, in which there are many connections, and many requests over these connections, the use of mTLS violates a number of core HTTP design assumptions (e.g. it explicitly violates the assumption that requests are independent and stateless, by adding connection-oriented state across requests, like NTLM/Kerberos do).
The core question here is “Can we make mTLS better”, and it’s important to realize that, in its present form, the flaws are deeper than just a preflight, and the preflight is a symptom of these design issues. The specs I mentioned try to tackle these design issues at their root, and notably, do so by acknowledging that servers must be prepared for requests to come in with the wrong/no credentials. This is why the “use client-certificate: optional” is actually a really reasonable and preferable solution: it helps align your auth layer to respect HTTP auth semantics, by having your application layer authenticate requests, rather than having your transport layer perform the authentication and your HTTP layer implicitly rely on it.
Firefox now has a Chrome-compatible mode that's disabled by default. But we might consider changing that if we keep running into issues. (Tracked by https://bugzilla.mozilla.org/show_bug.cgi?id=1019603.)
I'd be curious to hear from @youennf and others from Safari with regards to what their thinking is on this.
I do not think we should encourage this pattern, the spec status quo seems preferable to me. IIUIC, Chrome would ideally align with the spec so that it does not get deployed to more websites. That does not preclude enabling this behavior for specific existing websites.
But what I think is most important is will this change make security worse?
Yes. It deliberately introduces ambient auth for a channel/transport based auth method, which we have two decades of experience from NTLM/Kerberos to know this causes a host of security issues.
This ambient auth through client certificates is already here and functioning in all browser, but this is not the issue at hand. What we are talking about is if sending a certificate during the preflight will make security worse (as in the spec this is cited as the reason for not including them in the preflight in the first place).
Again:
- the preflight can be worked around by in all fetch'es replacing all
PUTs with content-typeapplication/jsontoPOSTs with content-typetext/plainto get the certificate prompt and to pass the data through, but this is unaesthetic --- why doesn't the spec allow me to do things correctly? - since a preflight is HTTP
OPTIONSrequest then it doesn't cause change in server state, thus it should be safe to use with credentials included (the "missiles will be fired" in the main request if-and-only-if the response to the preflight allowed us to do so)
- the preflight can be worked around by in all fetch'es replacing all
PUTs with content-typeapplication/jsontoPOSTs with content-typetext/plainto get the certificate prompt and to pass the data through, but this is unaesthetic --- why doesn't the spec allow me to do things correctly?
If you’re able to cause a prompt here, that’s a bug. I’ll try to reproduce in Chrome and see about fixing, if so, because that shouldn’t happen.
- since a preflight is HTTP
OPTIONSrequest then it doesn't cause change in server state, thus it should be safe to use with credentials included (the "missiles will be fired" in the main request if-and-only-if the response to the preflight allowed us to do so)
We don’t send credentials in preflights in preflights to ensure they’re not misinterpreted as “missiles will be fired”.
I understand and am aware that the core of the concern seems to be that because of the current preflight behavior, it means that if you’re serving TLS to a browser, you cannot rely on mandating transport level authentication, and must leave it optional, dealing with it at a request level (authenticating individual requests for resources). Setting aside all the other problematic aspects of mTLS (e.g. prompting, renegotiation, rejection for inadequate security), the current behavior preserves HTTP semantics and aligns with the HTTP authentication model. Moving to enable pure transport auth, which is what enabling it for preflights would do, moves away from those semantics, from other auth methods, and from all of the efforts to improve the mTLS experience (such as the aforementioned spec efforts).
It does sound like there’s no dispute that the client auth should continue to be considered credentials though, which is certainly essential for security, and that the only concern is including it in preflights so servers can force authentication at the transport layer. Is that right?
If you’re able to cause a prompt here, that’s a bug. I’ll try to reproduce in Chrome and see about fixing, if so, because that shouldn’t happen.
Are you sure? If I do a fetch with GET and include credentials (with fetch(.., {credentials: "include"}) then credentials should be included. The prompt is OK because similar to OPTIONS, a GET doesn't cause a state change.
We don’t send credentials in preflights in preflights to ensure they’re not misinterpreted as “missiles will be fired”.
Again, OPTIONS is similar to GET in this regard. No stateful thing should be done in reply to an OPTIONS request (other than logging the request and similar).
It does sound like there’s no dispute that the client auth should continue to be considered credentials though, which is certainly essential for security, and that the only concern is including it in preflights so servers can force authentication at the transport layer. Is that right?
Well, silently I considered making another issue for including all credentials with preflights (not just the TLS client certificates, but the ones in HTTP headers too). Because an OPTIONS request is a safe HTTP verb, and if the server allows credentialed cross-origin requests anyway (by replying positively to the preflight), then I don't see what the security benefit of not sending credentials in the preflight would be.
Are you sure?
Yes, we want to limit the sources of those prompts to clear and actionable user interactions (e.g. navigations). This state machine gets confused from time to time.
Because an
OPTIONSrequest is a safe HTTP verb,
In theory, yes, but a number of such features exist because “in practice” this is not the case, and servers are ill-prepared (e.g. examining the message semantics while ignoring the request method used). Part of the reason for the preflight in the first place was to make sure that the server does understand the semantics and handles appropriately, and the omission of credentials prevents against confused deputy issues.
Yes, we want to limit the sources of those prompts to clear and actionable user interactions (e.g. navigations). This state machine gets confused from time to time.
In theory, yes, but a number of such features exist because “in practice” this is not the case, and servers are ill-prepared (e.g. examining the message semantics while ignoring the request method used). Part of the reason for the preflight in the first place was to make sure that the server does understand the semantics and handles appropriately, and the omission of credentials prevents against confused deputy issues.
If this is the case then in the long term, will non-preflighted requests be removed entirely?
If this is the case then in the long term, will non-preflighted requsts be removed entirely?
No, they would behave like WebSockets do: if you’ve previously selected credentials for the host (e.g. navigation), then they’re available to be reused. If the user has not already provided credentials-that-require-interaction, however, they won’t be sent/the fetch would raise it as a network error (e.g. as requests through proxies can also do)
No, they would behave like WebSockets do: if you’ve previously selected credentials for the host (e.g. navigation), then they’re available to be reused. If the user has not already provided credentials-that-require-interaction, however, they won’t be sent/the fetch would raise it as a network error (e.g. as requests through proxies can also do)
Sorry, what I meant to ask was: will non-preflighted request go away entirely for cross-origin APIs? since one never navigates to them, but just use as a target for fetch requests.
And then perhaps another bug: currently there is the odd situation that when I run a fetch with GET on a certificate-required site then this succeeds (prompting me also for a certificate when it's the first time I touch that cross-origin API). And then if I follow that up with a PUT (which requires a preflight), then this fails -- although this was an origin I already had interacted with and provided a certificate for.
Some more clarifications:
I understand and am aware that the core of the concern seems to be that because of the current preflight behavior, it means that if you’re serving TLS to a browser, you cannot rely on mandating transport level authentication, and must leave it optional, dealing with it at a request level (authenticating individual requests for resources). Setting aside all the other problematic aspects of mTLS (e.g. prompting, renegotiation, rejection for inadequate security), the current behavior preserves HTTP semantics and aligns with the HTTP authentication model. Moving to enable pure transport auth, which is what enabling it for preflights would do, moves away from those semantics, from other auth methods, and from all of the efforts to improve the mTLS experience (such as the aforementioned spec efforts).
The client certificate prompt should appear on the first request made to the server -- I don't want any of the renegotiation, authentication of individual requests that you mention. (And AFAIK, it's not supported with the latest TLS anyway) I'd just like a prompt on the preflight, similar to how it successfully happens when I GET or POST the same API.
In theory, yes, but a number of such features exist because “in practice” this is not the case, and servers are ill-prepared (e.g. examining the message semantics while ignoring the request method used). Part of the reason for the preflight in the first place was to make sure that the server does understand the semantics and handles appropriately, and the omission of credentials prevents against confused deputy issues.
I don't think this is a reason. If the standards body doesn't take a stand then who will? And also, you're not adding any security because the attacker can launch missiles with a simple GET already, no need to trigger a preflight.
@sleevi Hi, I had another review of this thread and I still believe sending client certificates during a preflight imposes no security issue. To rehash:
-
OPTIONSrequest is as safe as aGET, andGETwith credentials is allowed by the spec - client certificate authentication doesn't appear to be a hack (where is this info coming from?) as it's the same X.509 certificate verification any browser does for a HTTPS website, but what now the server does for the client.
The Secondary Certificates and CATCH drafts seem unrelated as they are are either a solution to trigger TLS renegotiation with a HTTP header or request a certificate in an ongoing TLS session to avoid a full renegotiation. Prompting for a certificate in a preflight wants a mutually authenticated session from the start.
I have been reading through this thread, and wanted to verify my understanding of everything is correct. As things stand it seems like Client Certificate + CORS + HTTP/2 is not a valid combination of things to use. Is that right?
My understanding is that with CORS and Client Certificate the OPTIONS request can / should be done without the certificate based on the spec. And if you are using HTTP/2 the connection is not renegotiated after the OPTIONS request is complete. So when you get to an actual request, like a POST, it fails to validate the certificate since none was provided. This seems to be quite problematic as far as the specs go.
I certainly would like to take advantage of HTTP/2 benefits, but I have to use Client Certificates and CORS, and it doesn't work as I would have expected. I have experienced plenty of issues with Client Certificates before and the application works fine in both Chrome and Firefox via HTTP/1.1, but things stopped working in Firefox when I turned on HTTP/2. It took finding this thread to understand what was happening.
Even if I wanted to, I do not have control of browsers settings on every client machine to set network.cors_preflight.allow_client_cert on them. So its not really a viable option.
I am also trying to understand specifically with OPTIONS requests. Why is it that the spec requires me (the server) to respond to the request before they have mutually authenticated? That means I have to return information about the route to an unauthenticated client. That seems less than ideal.
I would expect a new connection to be made that does include the client certificate for the actual request. If that does not work in Firefox, please file a bug. This thread has already discussed your question at the end at length, so I'm not going to restate that here.
The original post in this thread seems to specifically call out the behavior I am discussing as invalid. Considering this a bug in Firefox does not seem to align with my reading of the rest of the thread. That is why I asked for clarification. If that is the case I can certainly work on filing a bug for it, but it will take me a bit to make a good standalone example.
I could be missing something but nothing in the thread seems to discuss the security implications from a server's perspective of responding to an OPTIONS request without having mutually authenticated that client making that request. There is debate about the safety of the OPTIONS request and the safety of including credentials from the client side. I was trying to add my perspective to that debate. And from my perspective an OPTIONS request is no riskier than a GET for the client and I can require mutual authentication for that GET but not for the OPTIONS request. As a server, this seems like legitimate security concern to me. I would rather not respond to any request without mutual authentication, but the spec does not seem to allow that. The only way to do that is drop Firefox support and utilize a bug in Chrome, which is certainly not something we should be relying on.
I guess I'm not following then. OPTIONS is indeed done without credentials, but that doesn't mean that the actual request should fail if the server properly responded to the OPTIONS request.
And no, the confused deputy attack referenced a couple times above is about the server, not the client.
I guess I'm not following then.
OPTIONSis indeed done without credentials, but that doesn't mean that the actual request should fail if the server properly responded to theOPTIONSrequest.
"properly responded" in this context means dropping the certificate requirement and making them optional. Is that really more secure?
@eseglem In REST OPTIONS is as safe as a GET or HEAD, so why they specifically forbid a certificate prompt for OPTIONS it isn't really clear.
I am also trying to understand specifically with
OPTIONSrequests. Why is it that the spec requires me (the server) to respond to the request before they have mutually authenticated? That means I have to return information about the route to an unauthenticated client. That seems less than ideal.
The intent of the spec is that OPTIONS is a public protocol to query the server about supporting modern standards (CORS). As such, the server must be willing to declare support for modern standards without requiring authentication of any kind (client certs or otherwise).
Using OPTIONS for this is a workaround for the historical mess we currently have.
And the OPTIONS workaround unfortunately necessitates making two separate connections when you use HTTP/2 because client certs are on so low level feature in the stack.
It currently appears that both Chrome and Firefox are finally going to align with the spec so if you maintain a server and want to use CORS, you must be prepared to only have optional transport level client certs.
Yes, it's painful but it is considered the most secure way to proceed given the historical mess we currently have.