Tuesday, February 25. 2025
Mixing up Public and Private Keys in OpenID Connect deployments
OpenID Connect is a single sign-on protocol that allows web pages to offer logins via other services. Whenever you see a web page that offers logins via, e.g., your Google or Facebook account, the technology behind it is usually OpenID Connect.
An OpenID Provider like Google can publish a configuration file in JSON format for services interacting with it at a defined URL location of this form: https://example.com/.well-known/openid-configuration (Google's can be found here.)
Those configuration files contain a field "jwks_uri" pointing to a JSON Web Key Set (JWKS) containing cryptographic public keys used to verify authentication tokens. JSON Web Keys are a way to encode cryptographic keys in JSON format, and a JSON Web Key Set is a JSON structure containing multiple such keys. (You can find Google's here.)
Given that the OpenID configuration file is at a known location and references the public keys, that gives us an easy way to scan for such keys. By scanning the Tranco Top 1 Million list and extending the scan with hostnames from SSO-Monitor (a research project providing extensive data about single sign-on services), I identified around 13.000 hosts with a valid OpenID Connect configuration and corresponding JSON Web Key Sets.
Confusing Public and Private Keys
JSON Web Keys have a very peculiar property. Cryptographic public and private keys are, in essence, just some large numbers. For most algorithms, all the numbers of the public key are also contained in the private key. For JSON Web Keys, those numbers are encoded with urlsafe Base64 encoding. (In case you don't know what urlsafe Base64 means, don't worry, it's not important here.)
Here is an example of an ECDSA public key in JSON Web Key format:
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"
}
Here is the corresponding private key:
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"
}
You may notice that they look very similar. The only difference is that the private key contains one additional value called d, which, in the case of ECDSA, is the private key value. In the case of RSA, the private key contains multiple additional values (one of them also called "d"), but the general idea is the same: the private key is just the public key plus some extra values.
What is very unusual and something I have not seen in any other context is that the serialization format for public and private keys is the same. The only way to distinguish public and private keys is to check if there are private values. JSON is commonly interpreted in an extensible way, meaning that any additional fields in a JSON field are usually ignored if they have no meaning to the application reading a JSON file.
These two facts combined lead to an interesting and dangerous property. Using a private key instead of a public key will usually work, because every private key in JSON Web Key format is also a valid public key.
You can guess by now where this is going. I checked whether any of the collected public keys from OpenID configurations were actually private keys.
This was the case for 9 hosts. Those included host names belonging to some prominent companies, including stackoverflowteams.com, stack.uberinternal.com, and ask.fisglobal.com. Those three all appear to use a service provided by Stackoverflow, and have since been fixed. (A report to Uber's bug bounty program at HackerOne was closed as a duplicate for a report they said they cannot show me. The report to FIS Global was closed by Bugcrowd's triagers as not applicable, with a generic response containing some explanations about OpenID Connect that appeared to be entirely unrelated to my report. After I asked for an explanation, I was asked to provide a proof of concept after the issue was already fixed. Stack Overflow has no bug bounty program, but fixed it after a report to their security contact.)
Short RSA Keys
7 hosts had RSA keys with a key length of 512 bit configured. Such keys have long been known to be breakable, and today, doing so is possible with relatively little effort. 45 hosts had RSA keys with a length of 1024 bit, which is considered to be breakable by powerful attackers, although such an attack has not yet been publicly demonstrated.
The first successful public attack on RSA with 512 bit was performed in 1999. Back then, it required months on a supercomputer. Today, breaking such keys is accessible to practically everyone. An implementation of the best-known attack on RSA is available as an Open Source software called CADO-NFS. Ryan Castellucci recently ran such an attack for a 512-bit RSA key they found in the control software of a solar and battery storage system. They mentioned a price of $70 for cloud services to perform the attack in a few hours. Cracking an RSA-512 bit key is, therefore, not a significant hurdle for any determined attacker.
Using Example Keys in Production
Running badkeys on the found keys uncovered another type of vulnerability. Before running the scan, I ensured that badkeys would detect example private keys in common Open Source OpenID Connect implementations. I discovered 18 affected hosts with keys that were such "Public Private Keys," i.e., keys where the corresponding private key is part of an existing, publicly available software package.
I have reported all 512-bit RSA keys and uses of example keys to the affected parties. Most of them remain unfixed.
Impact
Overall, I discovered 33 vulnerable hosts. With 13,000 detected OpenID configurations total, 0.25% of those were vulnerable in a way that would allow an attacker to access the private key.
How severe is such a private key break? It depends. OpenID Connect supports different ways in which authentication tokens are exchanged between an OpenID Provider and a Consumer. The token can be exchanged via the browser, and in this case, it is most severe, as it simply allows an attacker to sign arbitrary login tokens for any identity.
The token can also be exchanged directly between the OpenID Provider and the Consumer. In this case, an attack is much less likely, as it would require a man-in-the-middle attack and an additional attack on the TLS connection between the two servers. I have not made any attempts to figure out which methods the affected hosts were using.
How to do better
I would argue that two of these issues could have been entirely prevented by better specifications.
As mentioned, it is a curious and unusual fact that JSON Web Keys use the same serialization format for public and private keys. It is a design decision that makes confusing public and private keys likely.
In an ecosystem where public and private keys are entirely different — like TLS or SSH — any attempt to configure a private key instead of a public key would immediately be noticed, as it would simply not work.
One mitigation that can be implemented within the existing specification is for OpenID Connect implementations to check whether a JSON Web Key Set contains any private keys. For all currently supported values, this can easily be done by checking the presence of a value "d". (Not sure if this is a coincidence, but for both RSA and ECDSA, we tend to call the private key "d".)
The current OpenID Connect Discovery specification says: "The JWK Set MUST NOT contain private or symmetric key values." Therefore, checking it would merely mean that an implementation is enforcing compliance with the existing specification. I would suggest adding a requirement for such a check to future versions of the standard.
Similarly, I recommend such a check for any other protocol or software utilizing JSON Web Keys or JSON Web Key Sets in places where only public keys are expected.
It is probably too late to change the JSON Web Key standard itself for existing algorithms. However, in the future, such scenarios could easily be avoided by slightly different specifications. Currently, the algorithm of a JSON Web Key is configured in the "kty" field and can have values like "RSA" or "EC".
Let's take one of the future post-quantum signature algorithms as an example. If we specify ML-DSA-65 (I know, not a name easy to remember), instead of defining a "kty" value (or, as they seem to do it these days, the "alg" value) of "ML-DSA-65", we could assign two values: "ML-DSA-65-Private" and "ML-DSA-65-Public".
When it comes to short RSA keys, it is surprising that 512-bit keys are even possible in these protocols. OpenID Connect and everything it is based on is relatively new. The first draft version of the JSON Web Key standard dates back to 2012 — thirteen years after the first successful attack on RSA-512. Warnings that RSA-1024 was potentially insecure date back to the early 2000s, and at the time all of this was specified, there were widely accepted recommendations for a minimal key size of 2048 bit.
All of this is to say that those protocols should have never supported such short RSA keys. Ideally, they should have never supported RSA with arbitrary key sizes, and just defined a few standard key sizes. (The RSA specification itself comes from a time when it was common to allow a lot of flexibility in cryptographic algorithms. While pretty much everyone uses RSA keys in standard sizes that are multiples of 1024, it is possible to even use key sizes that are not aligned on bytes — like 2049 bit. The same could be said about the RSA exponent, which everyone sets to 65537 these days. Making these things configurable provides no advantage and adds needless complexity.)
Preventing the use of known example keys — I like to call them Public Private Keys — is more difficult to avoid. We could reduce these problems if people agreed to use a standardized set of test keys. RFC 9500 contains some test keys for this exact use case. My recommendation is that any key used as an example in an RFC document is a good test key. While that would not prevent people from using those test keys in production, it would make it easier to detect such cases.
You can use badkeys to check existing deployments for several known vulnerabilities and known-compromised keys. I added a parameter --jwk that allows directly scanning files with JSON Web Keys or JSON Web Key Sets in the latest version of badkeys.
Thanks to Daniel Fett for the idea to scan OpenID Connect keys and for feedback on this issue. Thanks to Sebastian Pipping for valuable feedback for this blogpost.
Image source: SVG Repo/CC0
Posted by Hanno Böck
in Cryptography, English, Security
at
12:08
| Comment (1)
| Trackbacks (0)
Defined tags for this entry: cryptography, itsecurity, openid, openidconnect, security, sso, websecurity
Related entries by tags:
- Private Keys in the Fortigate Leak
- How to create a Secure, Random Password with JavaScript
- File Exfiltration via Libreoffice in BigBlueButton and JODConverter
- Generating CRIME safe CSRF Tokens
- Userdir URLs like https://example.org/~username/ are dangerous
- Security Issues with PGP Signatures and Linux Package Management
- How my personal Bug Bounty Program turned into a Free Security Audit for the Serendipity Blog
- Introducing Snallygaster - a Tool to Scan for Secrets on Web Servers
- Some minor Security Quirks in Firefox
- Abandoned Domain Takeover as a Web Security Risk
Trackbacks
Trackback specific URI for this entry
No Trackbacks
Comments
Display comments as
(Linear | Threaded)
Thanks for your research on this!
I have a small addition to the “Impact” section: Some OpenID Providers also double as plain OAuth Authorization Servers, and probably most of them also use the same signing keys to sign the OAuth access tokens – A profile of this was specified in retrospect in RFC 9068.
If an attacker is able to create access tokens on his own, he can also impersonate any user at any API / resource accepting those tokens.
I have a small addition to the “Impact” section: Some OpenID Providers also double as plain OAuth Authorization Servers, and probably most of them also use the same signing keys to sign the OAuth access tokens – A profile of this was specified in retrospect in RFC 9068.
If an attacker is able to create access tokens on his own, he can also impersonate any user at any API / resource accepting those tokens.