X3DH密钥协商协议

密 码 学 技 术

实 验 报 告

实验名称 专题一 X3DH密钥协商协议

实验目的

  1. 了解X3DH协议内容;
  2. 理解代码;
  3. 编程实现简易版协议;

实验要求

  1. 搭建通信三方,包括一个服务器,两个客户端,客户端分别是Alice和Bob;
  2. Bob通过密码库或算法生成一对公私钥并保存,作为身份密钥;
  3. Bob再生成一对公私钥,作为预共享密钥;
  4. Bob使用身份密钥的私钥对预共享密钥的公钥签名;
  5. Bob将身份密钥的公钥、预共享密钥的公钥、预共享密钥的公钥签名打包,作为Bob的公钥包,发送到服务器,由服务器存储;

实验环境

  • PyCharm
  • Python 3.10

实验原理

DH协议:

假设Alice和Bob要确定一个消息密钥,DH协议的原理可以用下面的公式来表示:

DH(A的私钥,B的公钥) = 协商密钥S = DH(B的私钥,A的公钥)

DH协议算法需要2个参数:自己的私钥和对方的公钥。对于Alice和Bob来说,他们

只需知道对方的公钥,计算出的密钥S就是一样的。

DH协议的应用流程如下:

IMG_256

经典的密钥协商算法有Diffie-Hellman密钥协商方案:

公开域参数包括群(G,·)和阶为q的元素g∈G。假设有Alice,Bob两个参与者进行协商。

1) Alice选取一个随机数a(0≤a≤q-1),计算A=ga并将A发送给Bob。

2) Bob选取一个随机数b(0≤b≤q-1),计算B=gb并将B发送给Alice。

3) Alice计算K=(gb ) a ,Bob计算K=(ga ) b,显然K=(gb ) a=(ga ) b=gab为共享密钥。

计算性Diffie-Hellman问题通常表示为CDH,这个困难问题基于有限域上离散对数的难解性,即gx=u,求解x是困难的。基于椭圆曲线的 Diffie-Hellman协议也是基于有限域上离散对数的难解性,即nP=m,P是椭圆曲线上的基点,求解n是困难的。

X3DH协议:

“X3DH”(或“扩展三重 Diffie-Hellman”)密钥协议。X3DH 在基于公钥相互验证的两方之间建立共享密钥。X3DH 提供前向保密和加密可否认性。

X3DH 专为异步设置而设计,其中一个用户 (“Bob”) 处于脱机状态,但已将一些信息发布到服务器。另一个用户(“Alice”)希望使用该信息将加密数据发送给 Bob,并建立共享密钥以供将来通信。

角色:

X3DH 协议涉及三方:Alice、Bob 和服务器。

Alice 希望使用加密向 Bob 发送一些初始数据,并建立一个可用于双向通信的共享密钥。

Bob 希望允许像 Alice 这样的各方与他建立共享密钥并发送加密数据。但是,当 Alice 尝试执行此操作时,Bob 可能处于脱机状态。为了实现这一点,Bob 与某个服务器建立了关系。

服务器可以存储从 Alice 到 Bob 的消息,Bob 稍后可以检索这些消息。服务器还允许 Bob 发布一些数据,服务器将提供给 Alice 等各方。

密钥:

我们将重点介绍公钥。

每一方都有一个长期身份公钥(IKA 代表 Alice,IK B 代表 Bob)。

Bob 还有一个签名的预密钥 SPK B,他将定期更改它,以及一组一次性预密钥 OPK B,每个预密钥都用于单个 X3DH 协议运行。

在每次协议运行期间,Alice 都会使用公钥 EKA 生成一个新的临时密钥对。

成功运行协议后,Alice 和 Bob 将共享密钥 SK。

过程概述:

X3DH 有三个阶段:

  • Bob 将他的标识密钥和预密钥发布到服务器。
  • Alice 从服务器获取一个“预密钥包”,并使用它向 Bob 发送初始消息。
  • Bob 接收并处理 Alice 的初始消息。

发布密钥:

Bob 向服务器发布一组椭圆曲线公钥,其中包含:

  • Bob 的身份密钥 IKB
  • Bob 签名的预密钥 SPKB
  • Bob 的预密钥签名 Sig(IK B, Encode(SPKB))

Bob 只需将其身份密钥上传到服务器一次。但是,Bob 可能会在其他时间上传新的一次性预密钥(例如,当服务器通知 Bob 服务器的一次性预密钥存储越来越少时)。

Bob 还将每隔一段时间(例如每周一次或每月一次)上传新的签名预密钥和预密钥签名。新签名的预密钥和预密钥签名将替换以前的值。

上传新的签名预密钥后,Bob 可能会将与之前签名的预密钥对应的私钥保留一段时间,以处理使用它在传输过程中延迟的消息。最终,Bob 应该删除这个私钥,以保持前向保密

发送初始消息:

为了与 Bob 执行 X3DH 密钥协议,Alice 联系服务器并获取包含以下值的“预密钥包”:

  • Bob 的身份密钥 IKB
  • Bob 签名的预密钥 SPKB
  • Bob 的预密钥签名 Sig(IK B, Encode(SPKB))

服务器应提供 Bob 的一次性预密钥之一(如果存在),然后将其删除。如果服务器上 Bob 的所有一次性预密钥都已删除,则捆绑包将不包含一次性预密钥。

Alice 验证预密钥签名,如果验证失败,则中止协议。然后,Alice 使用公钥 EKA 生成一个临时密钥对。并会计算:

  • DH1 = DH(IK A, SPK B)
  • DH2 = DH(EK A, IK B)
  • DH3 = DH(EKA, SPK B)
  • SK = KDF(DH1 ||DH2 ||DH3)

下图显示了密钥之间的 DH 计算。请注意,DH1 和 DH2 提供相互身份验证,而 DH3 和 DH4 提供前向保密。

计算 SK 后,Alice 删除了她的临时私钥和 DH 输出。

然后,Alice 计算一个“关联数据”字节序列 AD,其中包含双方的身份信息:

AD = 编码 (IKA) ||编码(IKB)

Alice 可以选择向 AD 附加其他信息,例如 Alice 和 Bob 的用户名、证书或其他标识信息。

然后,Alice 向 Bob 发送了一条初始消息,其中包含:

  • Alice 的身份密钥 IKA
  • 爱丽丝的短暂钥匙 EKA
  • 标识符,说明 Alice 使用了 Bob 的哪些预密钥
  • 使用某些AEAD加密方案[4]加密的初始密文,使用AD作为关联数据,并使用加密密钥,该密钥是SK或由SK键控的某些加密PRF的输出。

初始密文通常是某些后 X3DH 通信协议中的第一条消息。换句话说,此密文通常具有两个角色,作为某个后 X3DH 协议中的第一条消息,以及作为 Alice 的 X3DH 初始消息的一部分。

发送后,Alice 可以继续使用 SK 或后 X3DH 协议中从 SK 派生的密钥与 Bob 通信,但须遵守第 4 节中的安全注意事项。

接收初始消息:

收到 Alice 的初始消息后,Bob 会从消息中检索 Alice 的身份密钥和临时密钥。Bob 还会加载他的身份私钥,以及与 Alice 使用的任何签名预密钥和一次性预密钥(如果有)对应的私钥。

使用这些键,Bob 重复上一节中的 DH 和 KDF 计算以派生 SK,然后删除 DH 值。

然后,Bob 使用 IK A 和 IKB 构造 AD 字节序列,如上一节所述。最后,Bob 尝试使用 SK 和 AD 解密初始密文。如果初始密文解密失败,则 Bob 将中止协议并删除 SK。

如果初始密文解密成功,则 Bob 的协议完成。Bob 删除了使用的任何一次性预密钥私钥,以实现前向保密。然后,Bob 可以继续在后 X3DH 协议中使用 SK 或派生自 SK 的密钥与 Alice 进行通信。

实验内容

Client

import socket, threading, os

from colorama import init, Fore

from generate_keys import *

from generate_prime import *

from Crypto.Cipher import AES

import sys

from datetime import datetime

try:

init()

print(Fore.RESET)

global message_key #chiave di cifratura/decifratura dei messaggi

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(('127.0.0.1', 8888))

nickname = input("Scegli il tuo nickname: ")

client.sendall(str(nickname).encode('utf-8'))

nicknames = [nickname]

secret_chat = 'secret_chat_' + nickname + '.txt'

public_keys = ["KEYS"]

def gen_message_key():

global message_key

global ep_secret_key

if nickname == nicknames[0]:

# Sono sul primo client

shk1 = shared_key(lt_pub_key_other_client, ep_secret_key, p)

shk2 = shared_key(ep_pub_key_other_client, ep_secret_key, p)

shk3 = shared_key(ep_pub_key_other_client, lt_secret_key, p)

else:

# Sono sul secondo client

shk1 = shared_key(ep_pub_key_other_client, lt_secret_key, p)

shk2 = shared_key(ep_pub_key_other_client, ep_secret_key, p)

shk3 = shared_key(lt_pub_key_other_client, ep_secret_key, p)

message_key = KDF(shk1, shk2, shk3)

def receive():

try:

while True:

from_server = client.recv(16777216).decode()

try:

from_server = eval(from_server) # trasformazione della stringa in una lista

except TypeError:

print('C\'è stato un problema durante la valutazione dei dati provenienti dal server. Per ulteriori informazioni consultare il file README. Termina la chat in entrambi i terminali client con KeyboardInterrupt (CTRL+C).')

sys.exit(0)

# Controllo del tipo di messaggio usando come flag il primo elemento della lista

if from_server[0] == "P-G":

# Ricezione dei numeri p e g dal server

global p

global g

p = int(from_server[1])

g = int(from_server[2])

p_chat = open('public_chat.txt','a')

p_chat.write(nickname + ' ha ricevuto il generatore ' + str(g) + ' e il numero primo ' + str(p) + ' da condividere con l\'altro client.\n\n')

p_chat.close()

global lt_public_key

global lt_secret_key

# generazione delle chiavi long term ed effimere

lt_public_key, lt_secret_key = pub_priv_keys(p, g, 16)

public_keys.append(lt_public_key)

global ep_public_key

global ep_secret_key

ep_public_key, ep_secret_key = pub_priv_keys(p, g, 16)

public_keys.append(ep_public_key)

p_chat = open('public_chat.txt','a')

p_chat.write(nickname + ' ha inviato la sua chiave pubblica long term e le sue chiavi effimere pubbliche al server.\n\n')

p_chat.write('Chiave pubblica long term di '+ nickname + ': ' + str(lt_public_key) + '\n\n')

p_chat.write('Chiave pubblica effimera di '+ nickname + ': ' + str(ep_public_key) + '\n\n')

p_chat.close()

s_chat = open(secret_chat,'a')

s_chat.write(nickname + ' ha memorizzato in locale la sua chiave segreta long term e le sue chiavi effimere segrete, oltre alle chiavi pubbliche già inviate al server.\n\n')

s_chat.write('Chiave segreta long term di '+ nickname + ': ' + str(lt_secret_key) + '\n\n')

s_chat.write('Chiave segreta effimera di ' + nickname + ': ' + str(ep_secret_key) + '\n\n')

s_chat.close()

client.sendall(str(public_keys).encode('utf-8')) # invio delle chiavi pubbliche al server (pacchetto KEYS)

elif from_server[0] == "MSG":

# Ricezione del mesaggio cifrato dall'altro client con relativa decifratura e stampa

global message_key

nonce = from_server[2]

cipher = AES.new(message_key, AES.MODE_EAX, nonce)

msg = cipher.decrypt(from_server[1])

if msg == 'end_chat':

system.exit(0)

p_chat = open('public_chat.txt','a')

p_chat.write(nickname + ' ha ricevuto il messaggio:\n ' + str(from_server) + '\n\n')

p_chat.close()

s_chat = open(secret_chat,'a')

s_chat.write(nickname + ' ha ricevuto un messaggio, costituito dal nonce ' + str(nonce) + ' e dal cipher text: ' + str(cipher) + ' cifrato con AES.\n\n')

s_chat.write(nickname + ' decifra con il segreto condiviso il cipher text, ottenendo il public text: ' + msg.decode('utf-8') + '\n\n')

s_chat.close()

now = datetime.now()

current_time = now.strftime("%H:%M:%S")

chat = open('chat.txt','a')

if nickname == nicknames[0]:

chat.write('[' + current_time + '] ' + nicknames[1] + ': ' + msg.decode('utf-8') + '\n\n')

else:

chat.write('[' + current_time + '] ' + nicknames[0] + ': ' + msg.decode('utf-8') + '\n\n')

chat.close()

print(Fore.CYAN + msg.decode() + Fore.RESET)

elif from_server[0] == "ENTRY":

# Collegamento tra i due client: vengono mandati i rispettivi nickname

print(Fore.YELLOW + from_server[1] + Fore.RESET)

nicknames.append(from_server[2])

nicknames.sort() # ordinamento così da differenziare i due client per il riutilizzo del codice

elif from_server[0] == "KEYS":

#Ricezione dele prime chiavi: in pos 1 si trova la chiave pubblica long term dell'altro client, pos 2 la chave effimera

global lt_pub_key_other_client

global ep_pub_key_other_client

lt_pub_key_other_client = int(from_server[1])

ep_pub_key_other_client = int(from_server[2])

gen_message_key() # Ricevute le chiavi, genero la message key

if nickname == nicknames[0]:

secret_chat1 = 'secret_chat_' + nickname +'.txt'

s_chat = open(secret_chat1,'a')

s_chat.write(nickname + ' ha ricevuto le chiavi pubbliche di ' + nicknames[1] + ' e può calcolare il segreto condiviso:\n' + str(message_key) + '\n\n')

s_chat.write(nickname + ' ha cancellato la chiave segreta effimera ' + str(ep_secret_key) + '.\n\n')

del ep_secret_key

s_chat.close()

else:

secret_chat2 = 'secret_chat_' + nickname +'.txt'

s_chat = open(secret_chat2,'a')

s_chat.write(nickname + ' ha ricevuto le chiavi pubbliche di ' + nicknames[0] + ' e può calcolare il segreto condiviso:\n' + str(message_key) + '\n\n')

s_chat.write(nickname + ' ha cancellato la chiave segreta effimera ' + str(ep_secret_key) + '.\n\n')

del ep_secret_key

s_chat.close()

else:

print(Fore.RED + "Errore sulle flag" + Fore.RESET)

except KeyboardInterrupt:

sys.exit(0)

def write_message():

try:

while True:

msg = input("")

data = str(msg)

global message_key

cipher = AES.new(message_key, AES.MODE_EAX)

nonce = cipher.nonce

ciphertext = cipher.encrypt(str(data).encode('utf-8'))

# creazione del pacchetto msg: ["MSG", msg_cifrato, nonce]

c_msg = ["MSG"]

c_msg.append(ciphertext)

c_msg.append(nonce)

client.sendall(str(c_msg).encode('utf-8'))

except KeyboardInterrupt:

sys.exit(0)

receive_thread = threading.Thread(target=receive)

receive_thread.start()

write_thread = threading.Thread(target=write_message)

write_thread.start()

receive_thread.join()

write_thread.join()

except KeyboardInterrupt:

sys.exit(0)

Server

import socket, threading

from generate_keys import *

from generate_prime import *

import time

from socket import error as SocketError

import errno

try:

p_chat = open('public_chat.txt','w')

p_chat.write('#################################################################################################################\n')

p_chat.write('## ##\n')

p_chat.write('## Questo file contiene la trascrizione della chat tra Alice e Bob dal punto di vista di un agente esterno. ##\n')

p_chat.write('## ##\n')

p_chat.write('#################################################################################################################\n\n\n')

p_chat.close()

chat = open('chat.txt','w')

chat.write('##################################################################################################################\n')

chat.write('## ##\n')

chat.write('## Questo file contiene la trascrizione della chat in chiaro e rappresenta il front-end. ##\n')

chat.write('## ##\n')

chat.write('##################################################################################################################\n\n\n')

chat.close()

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind(('127.0.0.1', 8888))

server.listen(5)

nicknames = []

clients = []

# Connessione con i due client

for _ in range(0, 2):

client, addr = server.accept()

nick = client.recv(16777216).decode('utf-8')

secret_chat = 'secret_chat_' + nick + '.txt'

nicknames.append(nick)

clients.append(client)

s_chat = open(secret_chat,'w')

s_chat.write('CHAT DAL PUNTO DI VISTA DI ' + nick.upper() +'.\n\n')

s_chat.close()

# Generazione di p e g da condividere ai due client

p_bit_length = 16

p = prime_generator(p_bit_length)

# SCELGO GENERATORE g DI Zp* (ricavato dal corso di crittografia)

number_of_generators = 10 # fra quanti generatori voglio scegliere casualmente? (Non serve a molto in realtà, probabilmente si può rimuovere)

generators = find_group_generators(p, number_of_generators) # lista di generatori

r = random.randint(0, len(generators) - 1) # scelgo un indice casuale

g = generators[r] # prendo il generatore corrispondente

p_g = []

p_g.append("P-G")

p_g.append(str(p))

p_g.append(str(g))

#print(p_g)

#inoltro di p e g ai client

clients[0].sendall(str(p_g).encode('utf-8'))

clients[1].sendall(str(p_g).encode('utf-8'))

time.sleep(0.2)

# Sincronizzazione dei due client

for i in range(0, 2):

data = ["ENTRY"]

msg = "Sei connesso con "

if i == 0:

nick = nicknames[1]

else:

nick = nicknames[0]

client = clients[i]

msg += str(nick)

data.append(msg)

data.append(nick)

client.sendall(str(data).encode('utf-8'))

time.sleep(0.5) # do il tempo per sincronizzarsi

# Classi con thread per l'inoltro dei messaggi tra i due client

class inoltroToSecond(threading.Thread):

def __init__(self):

threading.Thread.__init__(self)

def run(self):

secret_chat_A = 'secret_chat_' + nicknames[0] +'.txt'

secret_chat_B = 'secret_chat_' + nicknames[1] +'.txt'

while True:

client = clients[1]

try:

data = client.recv(16777216).decode('utf-8')

except SocketError as e:

if e.errno != errno.ECONNRESET:

raise

server.close()

p_chat = open('public_chat.txt','a')

p_chat.write('La chat è stata chiusa dai client.')

p_chat.close()

s_chat_A = open(secret_chat_A,'a')

s_chat_A.write('La chat è stata chiusa dai client.')

s_chat_A.close()

s_chat_B = open(secret_chat_B,'a')

s_chat_B.write('La chat è stata chiusa dai client.')

s_chat_B.close()

sys.exit(0)

#print(nicknames[1] + " ha mandato: " + data)

client = clients[0]

if not data:

p_chat = open('public_chat.txt','a')

p_chat.write('La chat è stata chiusa dai client.')

p_chat.close()

s_chat_A = open(secret_chat_A,'a')

s_chat_A.write('La chat è stata chiusa dai client.')

s_chat_A.close()

s_chat_B = open(secret_chat_B,'a')

s_chat_B.write('La chat è stata chiusa dai client.')

s_chat_B.close()

sys.exit(0)

else:

data = eval(data)

client.sendall(str(data).encode('utf-8'))

class inoltroToFirst(threading.Thread):

def __init__(self):

threading.Thread.__init__(self)

def run(self):

while True:

client = clients[0]

try:

data = client.recv(16777216).decode('utf-8')

except SocketError as e:

if e.errno != errno.ECONNRESET:

raise

server.close()

sys.exit(0)

#print(nicknames[0] + " ha mandato: " + data)

client = clients[1]

if not data:

sys.exit(0)

else:

data = eval(data)

client.sendall(str(data).encode('utf-8'))

a=inoltroToFirst(

b=inoltroToSecond()

a.start()

b.start()

except KeyboardInterrupt:

sys.exit(0)

运行程序

  1. 运行server端

屏幕截图 2023-10-31 234040

  1. 运行两个客户端
  2. 第一个输入a,会生成相应公钥对在服务器端;

屏幕截图 2023-10-31 234029

  1. 第二的输入b,会生成相应公钥对在服务器端;

屏幕截图 2023-10-31 234036

  1. 默认a与b通信,连接成功后可以通话
  2. a与b连接成功

参考资料

posted @ 2024-11-02 10:05  风花赏秋月  阅读(173)  评论(0)    收藏  举报