pythonexceptioncryptographyfernet

cryptography.exceptions.InvalidSignature: Signature did not match digest


I wrote an example of KDC Server, using the package cryptography.fernet.

I cannot understand why, randomly, sometimes it runs correctly and sometimes it ends with an exception: cryptography.exceptions.InvalidSignature: Signature did not match digest.

The keys are created once, at the startup of the main. So the issue seems not to be related to creation of different random keys.

Anyone could help me to detect what is wrong?

from cryptography.fernet import Fernet
import uuid
import pickle

def generate_challenge():
  return uuid.uuid4().bytes

def serialize(o):
  return pickle.dumps(o)

def deserialize(o):
  return pickle.loads(o)

class InitiateRequest:
  def __init__(self, initiator, responder, challenge):
    self.initiator = initiator    # IDa
    self.responder = responder    # IDb
    self.challenge =  challenge   # N1

class InitiateResponse:
  def __init__(self, session_key, initiator, responder, challenge):
    self.session_key = session_key  # Ks
    self.initiator = initiator      # IDa
    self.responder = responder      # IDb
    self.challenge = challenge      # N1

class InvitationForward:
  def __init__(self, session_key, initiator):
    self.session_key = session_key  # Ks
    self.initiator = initiator      # IDa

class KDCServer:
  def __init__(self, generator: Fernet):
    self.generator = generator
    self.map_keys = {} # user+key pairs

  def subscribe(self, id, key):
    self.map_keys[id] = key
    print("KDCServer: I'm registering key " + str(key) + " for " + id)

  def issue_session_key(self, r: InitiateRequest):
    session_key = self.generator.generate_key()
    response = InitiateResponse(session_key, r.initiator, r.responder, r.challenge) 
    invitation = InvitationForward(session_key, r.initiator)
    print(self.map_keys)
    print("KDCServer: I'm using " + str(self.map_keys[r.initiator]) + " as keyA_KDC")
    keyA_KDC = Fernet(self.map_keys[r.initiator]) 
    print("KDCServer: I'm using " + str(self.map_keys[r.responder]) + " as keyB_KDC")
    keyB_KDC = Fernet(self.map_keys[r.responder])
    print("KDCServer: I've just issued a session key for " + r.initiator + " and " + r.responder)
    return {
        keyA_KDC.encrypt(serialize(response)),   # E(Ka,[Ks|IDa|IDb|N1])
        keyB_KDC.encrypt(serialize(invitation))  # E(Kb,[Ks|IDa])
    }

class User:

  def __init__(self, id:str, key:bytes):
    self.id = id
    self.key = key
    self.session_keys = {}

  def initiate(self, responder):
    challenge = generate_challenge()
    print(self.id + ": Let's retrieve a session key to communicate with " + responder)
    # store request for matching
    self.request = InitiateRequest(self.id, responder, challenge)
    return self.request

  def match_request(self, check:InitiateResponse)->bool:
      return (self.request.challenge == check.challenge) and (self.request.initiator == check.initiator) and (self.request.responder == check.responder)
  
  def accept_response(self, response):
    print(self.id + ": I'm decrypting using my key " + str(self.key))
    check = deserialize(Fernet(self.key).decrypt(response))
    if self.match_request(check):
      self.session_keys[check.responder] = check.session_key # save session key Ks
      print(self.id + ": I've got the session key to communicate with " + check.responder)

  def accept_invitation(self, invitation):
    check = deserialize(Fernet(self.key).decrypt(invitation))
    print(self.id + ": I've accepted the invitation from " + check.initiator)
    self.session_keys[check.initiator] = check.session_key # save session key Ks

  def send_message(self, message, receiver):
    print(self.id + ": I'm sending this message " + str(message) + " using the session key " + str(self.session_keys[receiver]))
    return Fernet(self.session_keys[receiver]).encrypt(message)

  def receive_message(self, cyphered, sender):
    print(self.id + ": I'm decrypting a message using the session key " + str(self.session_keys[sender]))
    message = Fernet(self.session_keys[sender]).decrypt(cyphered)
    print(self.id + ": I've received the message " + str(message) + " from " + sender)

def main():
  alice_key = Fernet.generate_key()
  bob_key = Fernet.generate_key()

  alice = User('Alice', alice_key)
  bob = User('Bob', bob_key)

  server = KDCServer(Fernet)
  server.subscribe(alice.id, alice_key)
  server.subscribe(bob.id, bob_key)

  # Alice sends request to KDC to get a session key
  request = alice.initiate(bob.id)
  response, invitation = server.issue_session_key(request)

  # Alice accepts response from KDC and forwards invitation to Bob
  alice.accept_response(response)
  # Bob accepts invitation from Alice
  bob.accept_invitation(invitation)

  cyphered = bob.send_message(b"My secret message", alice.id)
  alice.receive_message(cyphered, bob.id)

if __name__ == "__main__":
    main()

Solution

  • The problem is caused by the Set returned in issue_session_key(). A Python Set is unordered, so response and invitation will be swapped in main() 50% of the time, causing the error. Use e.g. a Tuple instead of the Set. The Python Tuple is ordered:

    ...
    return (
        keyA_KDC.encrypt(serialize(response)),   # E(Ka,[Ks|IDa|IDb|N1])
        keyB_KDC.encrypt(serialize(invitation))  # E(Kb,[Ks|IDa])
    )
    ...