Skip to content

Instantly share code, notes, and snippets.

@slingamn
Created August 13, 2021 01:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slingamn/3f2fed196df5ef14d1316a1ffa9d59f8 to your computer and use it in GitHub Desktop.
Save slingamn/3f2fed196df5ef14d1316a1ffa9d59f8 to your computer and use it in GitHub Desktop.
Against SCRAM

Against SCRAM

I added support for the SCRAM-SHA-256 authentication mechanism to the Ergo IRC server, in response to demand for a reference implementation that clients could test against. Conversely, if you're implementing a server, I added an irctest server test covering it.

Nonetheless, this decision should not be taken as an endorsement of SCRAM. I recommend against implementing SCRAM-SHA-256 or any other SCRAM variant. Here's why.

The baseline: SASL PLAIN over TLS

The most common method of authenticating to an IRC server is SASL PLAIN over TLS. This is very simple and is more or less equivalent to the authentication mechanism used by mainstream websites. The client connects to the server using TLS; at this point, the server authenticates itself to the client by providing a verifiable TLS server certificate. (Although server deployment of verifiable TLS certificates has historically been spotty, this concern has receded in the era of Let's Encrypt.) Then the client authenticates itself to the server by sending the server its password, in plaintext, over the confidential and authenticated channel provided by TLS.

This scheme immediately inherits the security properties of TLS, which are very good:

  1. The server authenticates itself to the client using its certificate
  2. The authentication sequence is not replayable, because of safeguards internal to the TLS protocol
  3. The authenticated channel is secure against both passive and active attack; an attacker able to monitor traffic cannot learn the password, and an attacker able to modify traffic cannot hijack the authenticated connection

So, why use SCRAM? SCRAM increases implementation complexity and slows down the handshake in exchange for several purported benefits relative to PLAIN-over-TLS. The most interesting one is that the password is not sent to the server in plaintext during authentication (it will still be sent in plaintext during the account registration process, although this is theoretically eliminable). Here's how that works:

SCRAM

The basic idea of SCRAM is simple: a user's password constitutes a shared secret, which can then be used for mutual authentication in a way that doesn't expose the secret directly to a passive attacker (i.e. an attacker able to monitor traffic). Simplifying somewhat, each side sends a nonce and receives a MAC of the two nonces, with a key derived from the shared secret.

This has a number of desirable properties. Some level of confidentiality for the password is obtained even if an attacker can observe the traffic (however, see below). The protocol is also secure against replay attacks (since both server and client choose a unique nonce on each attempt), even without any additional guarantees from the transport layer.

SCRAM doesn't replace TLS

The first notable limitation of SCRAM is fairly obvious: these properties only secure it against passive attack. An active attacker who can modify traffic can simply wait for a successful authentication to complete, then hijack the connection. So if SCRAM is to be deployed, it should be together with TLS, in which case its advantages vis-a-vis a passive attacker become marginal. (There is still a small advantage in that PLAIN over TLS reveals the password length, whereas SCRAM over TLS doesn't, since message lengths depend only on the hash size and the nonce lengths.)

SCRAM transcripts are equivalent to password hashes

The strong cryptographic primitives and compositions (the hash function and HMAC) provide some security guarantees if a transcript of a SCRAM authentication is revealed: no cryptanalysis of the transcript should improve significantly on brute-force guessing of the derived key. The problem is that the transcript does provide enough information for an offline brute-force attack on the derived key --- and if the password is weak, this yields a viable offline brute-force attack on the password. This is a further caution against using SCRAM in settings where an attacker might be able to monitor the connection (for example, using SCRAM over plaintext, or with a TLS certificate that cannot be validated by the client, or in a threat model where the IRC server is less trusted than the network services node).

Consider the initial steps of the SCRAM protocol, simplified somewhat:

  1. Client sends a client nonce
  2. Server sends a server nonce, a password salt, and a PBDKF2 iteration count
  3. Client uses its stored password and the salt as inputs to PBKDF2, deriving the client key; it sends the HMAC of the two nonces, keyed with the client key

At this point, the server has not authenticated itself (within the SCRAM protocol itself) to the client, but is nonetheless in possession of an HMAC output (the client's challenge response), the corresponding HMAC input (the two nonces concatenated with some metadata), and a function mapping plaintext passwords to HMAC keys (the derivation function implied by the salt and the iteration count). This is enough information to verify a guessed password.

The only barrier to this provided by the design of SCRAM is the computational cost of PBKDF2, which should be regarded at best as defense-in-depth. If the underlying transport is open to monitoring (plain TCP under passive attack, or TLS with an unverifiable server certificate under MITM), an attacker can collect such a transcript and then attack the password offline (using standard software techniques for embarrassingly parallel problems, or GPUs or ASICs).

Essentially, a SCRAM transcript has the same sensitivity as a typical hashed password stored at rest. (It may be more sensitive, in fact, since PBDKF2 is significantly behind the state of the art in password hashing algorithms.) If you wouldn't expose your database of hashed passwords to the public, you shouldn't expose SCRAM transcripts either. Hence, typical deployments should ensure that SCRAM is running over TLS with a verifiable certificate; this recommendation appears in RFC 5802 itself.

SCRAM needs hardening

SCRAM adds appreciable implementation complexity relative to PLAIN, increasing the attack surface of an implementation in ways that may not be obvious. In addition to normal concerns about implementation bugs, the server's control over the iteration count parameter poses problems. For example, a MITM attacker or malicious server could send a very low iteration count in order to reduce the cost of offline password cracking. RFC 5802 alludes implicitly to this concern:

For the SCRAM-SHA-1/SCRAM-SHA-1-PLUS SASL mechanism, servers SHOULD announce a hash iteration-count of at least 4096.

and one of the SCRAM libraries I evaluated enforces this strictly on the client side: by default, it refuses to proceed on receiving an iteration count lower than 4096.

Conversely, the server can DoS the client by sending a very large iteration count. (I thank @grawity for drawing this issue to my attention.) Neither SCRAM library I evaluated had a safeguard against such an attack.

SCRAM is slow

Reconnection to IRC is widely perceived as slow. In fact, most of the slowness is due to conventions that are inessential to the IRC client-to-server protocol:

  1. Hostname lookup: it's conventional to do a reverse DNS lookup of an IRC client's IP address, which is then publicly displayed to other clients. This is contrary to modern user expectations of network services. It's also slow.
  2. Ident lookup: in a relic of the Internet of 1988, it's conventional to query a privileged port on the client's IP and request, in plaintext, information on the client's local UNIX username. On modern systems, this request generally fails by timing out, adding an additional delay.

In terms of computational load on the server, SCRAM is actually more efficient than a typical PLAIN authentication, because the expensive KDF step is done only once (when precomputing the hashed credential); during authentication, the only cryptographic operation performed is HMAC, which is very fast. (In contrast, when checking a password submitted with PLAIN against stored KDF results, the server must reapply the KDF every time.) Nonetheless, SCRAM has the potential to slow down IRC reconnections relative to PLAIN, because it necessarily imposes at least one additional roundtrip between client and server. On a mobile network --- once the extraneous hostname and ident lookups have been optimized out --- this cost could potentially be significant. In contrast, a client can conduct a full IRCv3 handshake, including capability negotiation and SASL PLAIN, within a single roundtrip after the establishment of the TLS connection --- assuming the client is willing to optimistically reuse the previously supported list of CAPs and fall back to a slower mechanism if the server's CAP support has changed.

What to use instead?

There are really two threat models here. One is for end users who are concerned about the security of their accounts. The solution here is straightforward: they should use SASL PLAIN with a strong, unique password, logging in exclusively over TLS with verification of the server certificate. Generating a suitable password is easy, e.g.

python3 -c "import secrets; print(secrets.token_urlsafe(16))"

The cases in which SCRAM offers any tangible benefit over PLAIN with a strong, unique password are few and marginal: if there is no password reuse, then a PLAIN authentication to a compromised server discloses only the password to that one server, which is already compromised. Hypothetically, one could imagine a server that is only temporarily or partially compromised, allowing exfiltration of the plaintext password in a way that later makes a difference. I don't find this very persuasive, but if this is a concern, a better solution would be SASL EXTERNAL with a TLS client certificate, over TLS v1.3 (since earlier versions of TLS send the client certificate in plaintext).

The other threat model is for server and client implementors, or server operators, who cannot guarantee the strength or uniqueness of passwords chosen by their users. In this case, it's also difficult to see any clear advantage for SCRAM; in fact, SCRAM may slightly weaken protections for hashed passwords at rest, since it mandates the use of a non-memory-hard KDF. If SCRAM is being considered as an alternative to providing TLS with a verifiable certificate, this is a mistake; sending user data in plaintext over the public Internet should be considered unacceptable, whether or not the data contains passwords.

@ecki
Copy link

ecki commented Jan 28, 2023

The „is slow“ part should maybe also mention that running PBKDF2 on client side (with reasonable strength) can be slower than on the server, especially for mobile clients.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment