Single roundtrip authentication
One of the things that we did in RavenDB 4.0 was support running on Linux, which meant giving up on Windows Authentication. To the nitpickers among you, I’m aware that we can do LDAP authentication, and we can do Windows Auth over HTTP/S on Linux. Let us say that given the expected results, all I can wish you is that you’ll have to debug / deploy / support such a system for real.
Giving up on Windows Authentication in general, even on Windows, is something that we did with great relief. Nothing like getting an urgent call from a customer and trying to figure out what a certain user isn’t authenticated, and trying to figure out the trust relationship between different domains, forests, DMZ and a whole host of other stuff that has utter lack of anything related to us. I hate those kind of cases.
In fact, I think that my new ill wish phrase will become “may you get a 2 AM call to support Kerberus authentication problems on a Linux server in the DMZ to a nested Windows domain”. But that might be going too far and damage my Karma.
That lead us to API Key authentication. And for that, we want to be able to get authenticate against the server, and get a token, which can then use in future requests. An additional benefit is that by building our own system we are able to actually own the entire thing and support it much better. A side issue here is that we need to support / maintain security critical code, which I’m not so happy about. But owning this also give me the option of doing things in a more optimized fashion. And in this case, we want to handle authentication in as few network roundtrips as possible, ideally one.
That is a bit challenging, since on the first request, we know nothing about the server. I actually implemented a couple of attempts to do so, but pretty much all of them were vulnerable to some degree after we did security analysis on them. You can look here for some of the details, so we gave that up in favor of mostly single round trip authentication.
The very first time the client starts, it will use a well known endpoint to get the server public key. When we need to authenticate, we’ll generate our own key pair and use it and the server public key to encrypt a hash of the secret key with the client’s public key, then send our own public key and the encrypted data to the server for authentication. In return, the server will validate the client based on the hash of the password and the client public key, generate an authentication token, encrypt that with the client’s public key and send it back.
Now, this is a bit complex, I’ll admit, and it is utterly unnecessary if the users are using HTTPS, as they should. However, there are actually quite a few deployments where HTTPS isn’t used, mostly inside organizations where they choose to deploy over HTTP to avoid the complexity of cert management / update / etc. Our goal is to prevent leakage of the secret key over the network even in the case that the admin didn’t setup things securely. Note that in this case (not using HTTPS), anyone listening on the network is going to be able to grab the authentication token (which is valid for about half an hour), but that is out of scope for this discussion.
We can assume that communication between client & server are not snoopable, given that they are using public keys to exchange the data. We also ensure that the size of the data is always the same, so there is no information leakage there. The return trip with the authentication token is also
A bad actor can pretend to be a server and fool a client into sending the authentication request using the bad actor’s public key, instead of the server. This is done because we don’t have trust chains. The result is that the bad actor now has the hash of the password with the public key of the client (which is also known). So the bad guy can try do some offline password guessing, we mitigate that using secret keys that are a minimum of 192 bits and generated using cryptographically secured RNG.
The bad actor can off course send the data as is to the server, which will authenticate it normally, but since it uses the client’s public key both for the hash validation and for the encryption of the reply, the bad actor wouldn’t be able to get the token.
On the client side, after the first time that the client hits the server, it will be able to cache the server public key, and any future authentication token refresh can be done in a single roundtrip.
Comments
I can see how "encrypt a hash of the secret key..." helps with the bad actor, but how should the real server validate request using secret hash + random/temp public key only? Am I missing something?
Yuriy, The idea is simple, the server know the secret, and it knows the public key of the client, so it can also compute the hash. If they match, the client knows the key. That means that the server then encrypt the token with the client key, and send it back. The bad actor in the middle can't decrypt the key, and can't change the public key it sent because otherwise they won't match.
I'm in no way an expert, but here's what I see:
So your secret hash just became the secret and it is still MITM'able
AFAIK, MITM protection during key exchange is not something you can achieve without some form of trust binding (authentication). Not DH* or any other key exchange scheme handles that (Signal protocol is MITM'able and thats state of the art today). Binding can be done by placing public key thumbprint into connection string and an raise error with suggestion to "fix" connection string when connecting to an untrusted server (kinda like SSH trusted hosts). But then it's probably better to customize certificate trust logic for a real https on the client instead of rolling another layer. This will protect all requests and not just some, preventing access token stealing.
Over plaintext or without peer authentication through binding, it seems a digest scheme, like SCRAM-SHA-256 is the only way to not leak the secret. I know there's a draft RFC 7804, but I only checked it briefly.
Did I really miss something?
Yuriy, The MITM can decrypt the hash, and it can forward the request to the server, BUT: - The hash is also generated using the client public key. - The MITM cannot generate a hash with its own public key without already knowing the secret. - After getting the reply from the server, the token is encrypted using the given public key, so the MITM cannot open it.
Thanks for the review.
Ok, so "a hash of the secret key with the client’s public key" is really a HMAC where secret is secret and public key is a message?
That will probably work on the wire, but requires storing secrets as cleartext and lacks forward secrecy with client keys.
And still, why not ssh-like https on by default? Everything over https, trivial to setup, things like HTTP/2 available, etc...
Yuriy, We strongly recommend users do that, obviously. But sometimes they don't. And we want to ensure that even in a bad configuration, we are still better than plain text over the wire.
Yuriy, There is another issue, though. Doing certs over HTTPS would probably be the most secured, and least convenient method to the users. So we use the shared secret in the connection string as that is what everyone else is doing, and since there are well known methods to handle that.
I think I totally get the usability/security tradeoffs here :)
But those who did invest and set up https will have their secrets stored cleartext, weakening security and removing forward secrecy provided by "real" TLS.
I actually meant to suggest something like this:
and maybe
I think this goes along with your "secure by default" motto.
Yuriy, We actually had that, see: https://github.com/ravendb/ravendb/blob/v1.0/Raven.Client.Lightweight/Document/DocumentStore.cs#L676
And what you are suggesting will work, as long as you have a single server. But it is awkward, and hard to manage. And when you have multiple servers, all coming & going as the admin add them to the pool, it become a lot more fragile.
Is it possible/practical to store certificate in a cluster?
Yuriy, Sure, but that is back to the shared secret. And certificate management in a MAJOR PITA.
My concern is that this is weakening security for those who did set up https. BTW, everybody else seem to be slowly going for SCRAM-*
Yuriy?
SCRAM-*
?And I'm not following how this weakens security.
I mean that it weakens security by adding requirement to store secrets cleartext, without any PBKDF/hash/salt, since raw secret is necessary to perform validation.
SCRAM-* as in SCRAM-SHA-256. I know it is more suitable in connection-oriented protocols, but there's draft RFC 7804 as I mentioned.
You could also sign your messages when using Plain HTTP in order to render token snooping useless, or use an unique timestamp that you can encrypt as part of the token on each call (if you don't mind keeping a small amount of session data server side per connection, but this harder to implement in server farm scenarios).
Yuriy, That is probable, but this is pretty much how it works everywhere, since that is how connection strings has gotten people ot use.
Oren, if you consider cleartext secret storage on the server acceptable, why not just use HMAC with nonce and TTL? You could use HS256 JWT or roll your own token format if you want to avoid JSON/JWT.
Yuriy, What would be the difference between the modes? And how will it prevent a bad actor from getting the token?
Hi Oren,
If you do it over plaintext http, bad actor can steal access token from subsequent api requests, so token lifetime is your only protection here anyway, IMO. But HMAC JWT will always work in a single roundtrip for token endpoint, no need to fetch anything from the server.
And on a second thought, you can probably just use HMAC JWT as the access token.
Yuriy, JWT is what is generated after the auth. We are talking here about the auth part.
Oren,
But why is a separate auth step necessary at all if you don't have any security delegation or trusted authority / relying party separation?
If both client and server already know the secret in cleartext, HMAC JWT with "exp" limits the exposure for stolen access tokens to within validity period, just like your server-issued token. And without any pre-flight requests.
Yuriy,
A) Your setup requires synchronized clocks to a high degree. B) The cost of doing a key lookup on every call is too high, better do it once at the auth step and precalculate all permissions.
Oren,
Yes, clock has to be correct (within some clock skew tolerance period, like 5 mins), but it is bad enough if it isn't anyway.
And you can easily cache token permissions by original token string bypassing parsing per request.
Comment preview