X3DH密钥协商协议
密 码 学 技 术
实 验 报 告
实验名称 专题一 X3DH密钥协商协议
实验目的
- 了解X3DH协议内容;
- 理解代码;
- 编程实现简易版协议;
实验要求
- 搭建通信三方,包括一个服务器,两个客户端,客户端分别是Alice和Bob;
- Bob通过密码库或算法生成一对公私钥并保存,作为身份密钥;
- Bob再生成一对公私钥,作为预共享密钥;
- Bob使用身份密钥的私钥对预共享密钥的公钥签名;
- Bob将身份密钥的公钥、预共享密钥的公钥、预共享密钥的公钥签名打包,作为Bob的公钥包,发送到服务器,由服务器存储;
- …
实验环境
- PyCharm
- Python 3.10
实验原理
DH协议:
假设Alice和Bob要确定一个消息密钥,DH协议的原理可以用下面的公式来表示:
DH(A的私钥,B的公钥) = 协商密钥S = DH(B的私钥,A的公钥)
DH协议算法需要2个参数:自己的私钥和对方的公钥。对于Alice和Bob来说,他们
只需知道对方的公钥,计算出的密钥S就是一样的。
DH协议的应用流程如下:

经典的密钥协商算法有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)
运行程序
- 运行server端


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

- 默认a与b通信,连接成功后可以通话
- a与b连接成功
参考资料
- FrancescoCecconello/net_sec_whatsapp_signal_protocol: Implementazione rudimentale dell'algoritmo X3DH utilizzato da WhatsApp e Signal per garantire la segretezza delle conversazioni. (github.com)
- 即时通讯安全篇(十一):IM聊天系统安全手段之传输内容端到端加密技术 - im中国人 - 博客园 (cnblogs.com)
- 双棘轮算法:端对端加密安全协议,原理以及流程详解 - 简书 (jianshu.com)
- X3DH 密钥协议>>信号>>规范 (signal.org)X3DH 密钥协议>>信号>>规范 (signal.org)
浙公网安备 33010602011771号