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.