Search code examples
python-3.xserializationcryptographydiffie-hellman

Python Diffie-Hellman exchange cryptography library. Shared key not the same


Following the tutorial listed on the cryptography library documentation I have successfully created a function that demonstrates a Diffie-Hellman exchange. I am now trying to create a proof of concept socket server and socket client.

An undocumented requirement of this application is the sending of the public key to the client. This requires the DHPublicKey object to be serialised and serialised in order for it to be sent over the socket.

By doing this however the shared keys are not the same! I have tried to use different encoding formats (PEM for example) to see if this made a difference. Unfortunatly it has not. I get a different shared key on both sides. Here is an example of my code.

Client

parameters             = dh.generate_parameters(generator=2, key_size=1024, backend=default_backend())
client_private_key     = parameters.generate_private_key()
client_public_key      = client_private_key.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)

# Recv Server Pub key
length            = s.recv(2) # Prepend the length of the message
server_public_key = s.recv(int.from_bytes(length, "big"))
print("Got server public key: " + str(server_public_key))
server_public_key = load_der_public_key(server_public_key, default_backend())

# Send Pub key
s.send(len(client_public_key).to_bytes(2, "big") + client_public_key)

print("Generating shared key...")
shared_key = client_private_key.exchange(server_public_key)
print("Our shared key!: " + str(shared_key))

Server

parameters = dh.generate_parameters(generator=2, key_size=1024, backend=default_backend())
server_private_key   = parameters.generate_private_key()
server_public_key    = server_private_key.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)

newsocket.send(len(server_public_key).to_bytes(2, "big") + server_public_key)
print("Sent server public key: " + str(server_public_key))

length            = newsocket.recv(2) # Prepend the length of the message
client_public_key = newsocket.recv(int.from_bytes(length, "big"))
client_public_key = load_der_public_key(client_public_key, default_backend())

# Send the public key to the client
shared_key = server_private_key.exchange(client_public_key)

print("Our shared key is: " + str(shared_key))

As stated, I'm using the Python 3 Library Cryptography and use the following imports

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import dh
from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding, load_der_public_key

Also, putting the code into one file (Without network serialization) it works! the key is the same! Here is an example of the working code

print("SERVER: Performing DH exchange. DH 2048-bit key size")
parameters = dh.generate_parameters(generator=2, key_size=2048, backend=default_backend()) # Generate a 256-byte key

print("SERVER: Generating server private and public keys")
server_private_key     = parameters.generate_private_key()
server_peer_public_key = server_private_key.public_key()

print("CLIENT: Generating private and public keys")
client_private_key     = parameters.generate_private_key()
client_peer_public_key = client_private_key.public_key()

print("SERVER: Sending public key to client")
print("CLIENT: Sending public key to server")

server_shared_key = server_private_key.exchange(client_peer_public_key)
client_shared_key = client_private_key.exchange(server_peer_public_key)
print("server key is: " + str(server_shared_key))
print("client key is: " + str(client_shared_key))

What am I doing wrong when serialising or deserialising the key?


Solution

  • Your problem isn’t to do with serializing and deserializing the key, it is because you are generating different DH parameters on the server and the client. They need to be the same for Diffie Hellman to work.

    You could generate the parameters on the server and send them to the client, but a better option is to use a set of predefined parameters, for example group 14 defined in RFC 3526.

    To do that, change the line

    parameters = dh.generate_parameters(generator=2, key_size=1024, backend=default_backend())
    

    in both client and server to:

    p = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF
    g = 2
    
    params_numbers = dh.DHParameterNumbers(p,g)
    parameters = params_numbers.parameters(default_backend())
    

    Now both client and server will be using the same set of parameters and the key agreement will work. It will also be much faster, parameter generation is a costly process.

    Your code in a single file works because you only generate one set of parameters that is used by both sides of the exchange.