2

I'd like for two machines on a network to be able to prove to each other that they both have knowledge of a pre-shared secret, without revealing the secret to each other. Let's assume that all traffic over the connection between the parties, A and B, is encrypted.

Here are the steps I'm currently imagining:

1. A->B: nonce_A, hash(nonce_A || secret_key)

  • B checks that they can produce the same hash using nonce_A
  • even if B can produce the hash, it doesn't yet prove A knows the key (e.g.could be a replay attack)

2. B->A: nonce_B, hash(nonce_B || nonce_A || secret_key)

  • A checks that they can produce the same hash using the nonces and key
  • if nonce_B == nonce_A, the handshake fails
  • A now knows B knows the secret key

3. A->B: hash(nonce_A || nonce_B || secret_key)

  • B checks that they can produce the same hash using the nonces and key
  • B now knows A knows the secret key

4. Both sides know the key!

My understanding is that this protects against replay attacks (since a fresh nonce is used each time) and also against reflection attacks (since the hash must include BOTH parties' nonces).

Are there any security issues in this approach?

(I'm also curious if there are any existing RFC or similar for doing this.)

Kira
  • 21
  • 3
  • 2
    This question is off-topic here and on-topic at [cryptography.se]. "I'm also curious if there are any existing RFC or similar for doing this." - sure, just search for pre shared key authentication rfc and you'll find standards for PSK authentication in IPSec/IKE, EAP-PSK, PSK in TLS ... – Steffen Ullrich May 05 '23 at 19:17
  • 1
    One nitpick with this protocol is that it doesn't really authenticate the parties. Maybe that is desired? If it is not desired, then the issue is that an attacker can force party A into a session with itself while A thinks it completed the session with B. Depending on the application, this might not be so good. The protocol described is very close to the MAP1 protocol from Bellare and Rogaway (check it out). Another thing, using a Pseudo Random Function might be a better choice here. Though currently it's not too bad because the key is prepended. – Marc Ilunga May 10 '23 at 21:43
  • Thanks Marc, that's a great point. I think if each side had even a short-lived "session identifier" that was used in conjunction with the nonce, it would allow A to detect a replay attempt. And thanks for the references. – Kira May 17 '23 at 22:56
  • Marc, what is the danger in not using a PRF instead of (inside of?) the hash function? – Kira May 17 '23 at 23:09

1 Answers1

1

First off, if you can, just use something standard like TLS-PSK which is designed to do exactly this and is hard to mess up.

If you do want to roll your own crypto, symmetric stuff is definitely the most foolproof place to start.

simplifying the protocol

Simplest protocol is:

secret key sk, A and B pick nonces N_a,N_b

  • A-->B N_a
  • B-->A N_b,Hash(N_a || N_b || sk || bytes("b-->a"))
  • A-->B Hash(N_a || N_b || sk || bytes("a-->b"))

When deriving keys, pseudorandom values or challenge responses, just add a string to the hash that's unlikely to be reused elsewhere.

If you need to derive encryption keys, just do K_a2b=Hash(N_a || N_b || sk || bytes("K_a2b")) or similar. Adding strings to the value to be hashed is pretty standard for adding context to get different values out of a hash function or key derivation function.

A few things to keep in mind

Are you are using /dev/urandom or your platform's equivalent secure random byte source? If not, nonce values could be predictable.

If the random number generator in A or B breaks and nonces start repeating, hash comparisons can be vulnerable to a timing attack.

If you use your language's = operator to compare the hash value you receive from the peer to a value calculated locally your code will take longer to reject an answer if more bytes of the hash match. With enough timing measurements an attacker can determine the entire value. Always use something like https://docs.python.org/3/library/hmac.html#hmac.compare_digest when doing byte string comparisons in crypto code.

Alternatively, if you can't find a constant time comparison somewhere in your standard library, you can do this:

def masked_compare(a,b):
    rand=get_random_bytes(16)
    return hash(rand+a)==hash(rand+b)

It's not constant time but it doesn't allow for timing based extraction of the correct answer.

Richard Thiessen
  • 1,741
  • 9
  • 13
  • Thank you for your notes Richard! Also, TLS-PSK does a LOT more than just verifying each party knows a pre-shared key. I'm trying to see what exists that does only this piece. – Kira May 10 '23 at 18:10
  • I think this is vulnerable to the same replay attack Marc outlined in the comment to my question post. If bytes("a-->b") were actual session/permanent identities rather than a static string literal, I think it would be avoided. – Kira May 17 '23 at 23:08
  • 1
    Relay attack, not Replay, but yes. There's no session establishment happening here, at least I'd hope not. All you get is a liveness guarantee. Mallory needs to relay messages between two people who know the secrets. If you trust the underlying network (bad idea unless you're dealing with a secure VPN like Wireguard or similar.) then you could bind (EG:10.1.2.3 is Alice and 10.2.3.4 is Bob) as you suggest. If this is a prelude to sensitive communication then STOP IMMIDIATELY and use TLS or similar to encrypt that communication. – Richard Thiessen May 19 '23 at 03:24