GOSTIM:在一个晚上使用GOST密码进行P2P F2F E2EE IM

作为PyGOST库(纯Python中的GOST加密原语)的开发人员,我经常遇到有关如何在膝盖上实现最简单的安全消息传递的问题。 许多人认为应用密码术是一件相当简单的事情,对块密码的.encrypt()调用足以在通信通道上安全发送。 其他人则认为应用密码学是少数人的命运,可以接受的是,富裕的公司(例如带有数学奥林匹克竞赛的Telegram) 无法实现安全协议。

所有这些促使我写这篇文章,以表明加密协议和安全IM的实现并不是一项艰巨的任务。 但是,发明自己的身份验证和密钥协商协议是不值得的。

听证会

本文将使用专有的GOST加密算法PyGOST库和SIGMA-I身份验证和密钥协议协议(基于IPsec IKE实现)编写为点对点朋友到朋友端到端的加密即时通讯程序。带有PyDERASN库的消息的ASN.1编码(我之前已经写过 )。 先决条件:必须非常简单,可以在一个晚上(或工作日)从头开始编写,否则,它不再是一个简单的程序。 它可能有错误,不必要的困难,缺点,而且这是我使用asyncio库的第一个程序。

设计即时通讯


首先,您需要了解我们的IM外观。 为简单起见,让它成为一个对等网络,而不会发现任何参与者。 我们将亲自指出到哪个地址:用于与对话者通信的端口。

我了解,目前,假设两台任意计算机之间可以直接通信是对IM在实践中的适用性的重大限制。 但是,开发人员实施各种NAT遍历拐点的人越多,我们在IPv4 Internet上停留的时间就越长,任意计算机之间进行通信的可能性就会降低。 那么,您在家里和工作中可以忍受多少IPv6缺乏?

我们将有一个朋友对朋友的网络:应该事先知道所有可能的对话者。 首先,它极大地简化了一切:介绍自己,发现或未找到名称/密钥,断开连接或继续工作,了解对话者。 其次,在一般情况下,它是安全的并且排除了许多攻击。

IM接口将接近无精打采项目的经典解决方案,我非常喜欢它们的极简主义和Unix-way哲学。 每个对话者的IM程序都会创建一个带有三个Unix域套接字的目录:

  • in-发送给对话者的消息记录在其中;
  • out-从对话者收到的消息被从中读取;
  • 状态-从中读取信息,我们会发现对话者现在是否已连接,连接地址/端口。

此外,还会创建一个conn套接字,将其写入到哪个主机端口,从而启动与远程对话者的连接。

 |-爱丽丝
 |  |-在
 |  |-出
 |  `-状态
 |-鲍勃
 |  |-在
 |  |-出
 |  `-状态
 `-conn

这种方法允许您独立实现IM传输和用户界面,因为没有口味和颜色的朋友,所以您不会取悦所有人。 使用tmux和/或multitail ,您可以获得具有语法高亮显示的多窗口界面。 使用rlwrap,可以获得用于输入消息的GNU Readline兼容字符串。

实际上,无吮吸项目使用FIFO文件。 就个人而言,我无法理解在没有选定线程手工制作的情况下,如何以异步方式竞争性地处理文件(我很长时间以来一直在使用Go语言)。 因此,我决定不使用Unix域套接字。 不幸的是,这使得无法执行echo 2001:470:dead :: babe 6666> conn。 我使用socat解决了这个问题:echo 2001:470:dead ::宝贝6666 | socat-UNIX-CONNECT:conn,socat READLINE UNIX-CONNECT:alice / in。

初始不安全协议


TCP被用作一种传输方式:它保证交付及其顺序。 UDP不能保证一个或另一个(这在应用密码术时将很有用),并且Python中的SCTP支持是开箱即用的。

不幸的是,在TCP中没有消息的概念,而只有字节流。 因此,有必要提出一种消息格式,以便可以在此流中在彼此之间共享消息。 我们可以同意使用换行符。 对于初学者来说,这是合适的,但是,当我们开始加密邮件时,此符号可能会出现在密文中的任何位置。 因此,协议在网络上很流行,首先发送消息的长度(以字节为单位)。 例如,在Python中,开箱即用的是xdrlib,它使您可以使用类似的XDR格式。

我们无法正确有效地处理TCP读取-我们简化了代码。 我们以无休止的循环从套接字读取数据,直到解码完整的消息为止。 您也可以将JSON和XML用作此方法的格式。 但是,当添加加密时,则必须对数据进行签名和验证-这将需要逐字节的对象相同表示,而JSON / XML不提供这种表示(转储可能会有所不同)。

XDR适合执行此任务,但是,我选择具有DER编码和PyDERASN库的ASN.1,因为我们手头有高级对象,使用它们通常更轻松,更方便。 与无模式的bencodeMessagePackCBOR不同 ,ASN.1将根据硬编码的模式自动验证数据。

# Msg ::= CHOICE { # text MsgText, # handshake [0] EXPLICIT MsgHandshake } class Msg(Choice): schema = (( ("text", MsgText()), ("handshake", MsgHandshake(expl=tag_ctxc(0))), )) # MsgText ::= SEQUENCE { # text UTF8String (SIZE(1..MaxTextLen))} class MsgText(Sequence): schema = (( ("text", UTF8String(bounds=(1, MaxTextLen))), )) # MsgHandshake ::= SEQUENCE { # peerName UTF8String (SIZE(1..256)) } class MsgHandshake(Sequence): schema = (( ("peerName", UTF8String(bounds=(1, 256))), )) 

收到的消息将为Msg:文本MsgText(到目前为止有一个文本字段)或握手消息MsgHandshake(在其中传输对话者的名称)。 现在看起来过于复杂,但这是对未来的挑战。

      ┌─────┐┌─────┐
      │PeerA││PeerB│
      └──┬──┘└──┬──┘
         │信息握手(IdA)│
         │─────────────
         ││
         │信息握手(IdB)│
         │<─────────────
         ││
         │MsgText()│
         │─────────────
         ││
         │MsgText()│
         │<─────────────
         ││


没有加密的即时消息


就像我说过的,对于所有使用套接字的操作,都会使用asyncio库。 声明我们在发布时的期望:

 parser = argparse.ArgumentParser(description="GOSTIM") parser.add_argument( "--our-name", required=True, help="Our peer name", ) parser.add_argument( "--their-names", required=True, help="Their peer names, comma-separated", ) parser.add_argument( "--bind", default="::1", help="Address to listen on", ) parser.add_argument( "--port", type=int, default=6666, help="Port to listen on", ) args = parser.parse_args() OUR_NAME = UTF8String(args.our_name) THEIR_NAMES = set(args.their_names.split(",")) 

设置自己的名字(-我们的名字爱丽丝)。 逗号列出了所有预期的对话者(-他们的名字bob,eve)。 对于每个对话者,都会创建一个带有Unix套接字的目录,以及每个in,out状态的协程:

 for peer_name in THEIR_NAMES: makedirs(peer_name, mode=0o700, exist_ok=True) out_queue = asyncio.Queue() OUT_QUEUES[peer_name] = out_queue asyncio.ensure_future(asyncio.start_unix_server( partial(unixsock_out_processor, out_queue=out_queue), path.join(peer_name, "out"), )) in_queue = asyncio.Queue() IN_QUEUES[peer_name] = in_queue asyncio.ensure_future(asyncio.start_unix_server( partial(unixsock_in_processor, in_queue=in_queue), path.join(peer_name, "in"), )) asyncio.ensure_future(asyncio.start_unix_server( partial(unixsock_state_processor, peer_name=peer_name), path.join(peer_name, "state"), )) asyncio.ensure_future(asyncio.start_unix_server(unixsock_conn_processor, "conn")) 

来自用户的in套接字中的消息被发送到队列IN_QUEUES:

 async def unixsock_in_processor(reader, writer, in_queue: asyncio.Queue) -> None: while True: text = await reader.read(MaxTextLen) if text == b"": break await in_queue.put(text.decode("utf-8")) 

来自对话者的消息被发送到OUT_QUEUES队列,数据从该队列写入out套接字:

 async def unixsock_out_processor(reader, writer, out_queue: asyncio.Queue) -> None: while True: text = await out_queue.get() writer.write(("[%s] %s" % (datetime.now(), text)).encode("utf-8")) await writer.drain() 

从状态套接字读取时,该程序在PEER_ALIVE词典中查找对话者的地址。 如果还没有与对话者的连接,则会写入一个空行。

 async def unixsock_state_processor(reader, writer, peer_name: str) -> None: peer_writer = PEER_ALIVES.get(peer_name) writer.write( b"" if peer_writer is None else (" ".join([ str(i) for i in peer_writer.get_extra_info("peername")[:2] ]).encode("utf-8") + b"\n") ) await writer.drain() writer.close() 

将地址写入conn套接字后,将启动连接的“启动器”功能:

 async def unixsock_conn_processor(reader, writer) -> None: data = await reader.read(256) writer.close() host, port = data.decode("utf-8").split(" ") await initiator(host=host, port=int(port)) 

考虑启动器。 首先,他显然打开了与指定主机/端口的连接,并发送一个带有他的名字的握手消息:

  130 async def initiator(host, port): 131 _id = repr((host, port)) 132 logging.info("%s: dialing", _id) 133 reader, writer = await asyncio.open_connection(host, port) 134 # Handshake message {{{ 135 writer.write(Msg(("handshake", MsgHandshake(( 136 ("peerName", OUR_NAME), 137 )))).encode()) 138 # }}} 139 await writer.drain() 

然后,它等待远程方的响应。 尝试根据Msg ASN.1方案对接收到的响应进行解码。 我们假定整个消息将由一个TCP段发送,并且在调用.read()时将以原子方式接收它。 我们验证是否确实收到了握手消息。

  141 # Wait for Handshake message {{{ 142 data = await reader.read(256) 143 if data == b"": 144 logging.warning("%s: no answer, disconnecting", _id) 145 writer.close() 146 return 147 try: 148 msg, _ = Msg().decode(data) 149 except ASN1Error: 150 logging.warning("%s: undecodable answer, disconnecting", _id) 151 writer.close() 152 return 153 logging.info("%s: got %s message", _id, msg.choice) 154 if msg.choice != "handshake": 155 logging.warning("%s: unexpected message, disconnecting", _id) 156 writer.close() 157 return 158 # }}} 

我们确认与我们交谈的人的名字是我们已知的。 如果不是,则断开连接。 我们检查是否已经与他建立了连接(对话者再次给出了连接到我们的命令)并关闭了它。 带有消息文本的Python字符串放置在IN_QUEUES队列中,但是有一个特殊值None,该值指示msg_sender向协程停止工作,因此她将忘记与过时的TCP连接有关的编写器。

  159 msg_handshake = msg.value 160 peer_name = str(msg_handshake["peerName"]) 161 if peer_name not in THEIR_NAMES: 162 logging.warning("unknown peer name: %s", peer_name) 163 writer.close() 164 return 165 logging.info("%s: session established: %s", _id, peer_name) 166 # Run text message sender, initialize transport decoder {{{ 167 peer_alive = PEER_ALIVES.pop(peer_name, None) 168 if peer_alive is not None: 169 peer_alive.close() 170 await IN_QUEUES[peer_name].put(None) 171 PEER_ALIVES[peer_name] = writer 172 asyncio.ensure_future(msg_sender(peer_name, writer)) 173 # }}} 

msg_sender接受传出消息(从in套接字排队),将它们序列化为MsgText消息,然后通过TCP连接发送它们。 它随时可能中断-我们显然正在拦截它。

 async def msg_sender(peer_name: str, writer) -> None: in_queue = IN_QUEUES[peer_name] while True: text = await in_queue.get() if text is None: break writer.write(Msg(("text", MsgText(( ("text", UTF8String(text)), )))).encode()) try: await writer.drain() except ConnectionResetError: del PEER_ALIVES[peer_name] return logging.info("%s: sent %d characters message", peer_name, len(text)) 

最后,启动器进入一个无穷循环,从套接字读取消息。 检查这是否为文本消息,并将将它们从中发送到相应对话者的out套接字的队列放在OUT_QUEUES中。 您为什么不能只做.read()并解码消息? 因为有可能来自用户的几条消息将聚集在操作系统的缓冲区中,并由一个TCP段发送。 我们可以解码第一个,然后随后的一部分可以保留在缓冲区中。 在任何紧急情况下,我们都将关闭TCP连接并停止msg_sender协程(通过将None发送到OUT_QUEUES队列)。

  174 buf = b"" 175 # Wait for test messages {{{ 176 while True: 177 data = await reader.read(MaxMsgLen) 178 if data == b"": 179 break 180 buf += data 181 if len(buf) > MaxMsgLen: 182 logging.warning("%s: max buffer size exceeded", _id) 183 break 184 try: 185 msg, tail = Msg().decode(buf) 186 except ASN1Error: 187 continue 188 buf = tail 189 if msg.choice != "text": 190 logging.warning("%s: unexpected %s message", _id, msg.choice) 191 break 192 try: 193 await msg_receiver(msg.value, peer_name) 194 except ValueError as err: 195 logging.warning("%s: %s", err) 196 break 197 # }}} 198 logging.info("%s: disconnecting: %s", _id, peer_name) 199 IN_QUEUES[peer_name].put(None) 200 writer.close() 66 async def msg_receiver(msg_text: MsgText, peer_name: str) -> None: 67 text = str(msg_text["text"]) 68 logging.info("%s: received %d characters message", peer_name, len(text)) 69 await OUT_QUEUES[peer_name].put(text) 

让我们回到主要代码。 创建所有协程后,在启动程序时,我们将启动TCP服务器。 对于每个已建立的连接,他都会创建一个响应程序协程。

 logging.basicConfig( level=logging.INFO, format="%(levelname)s %(asctime)s: %(funcName)s: %(message)s", ) loop = asyncio.get_event_loop() server = loop.run_until_complete(asyncio.start_server(responder, args.bind, args.port)) logging.info("Listening on: %s", server.sockets[0].getsockname()) loop.run_forever() 

响应者类似于发起者,并且镜像所有相同的动作,但是为简单起见,读取消息的无限循环立即开始。 现在,握手协议从双方发送一条消息,但是将来,连接的发起方将发送两条消息,此后可以立即发送文本消息。

  72 async def responder(reader, writer): 73 _id = writer.get_extra_info("peername") 74 logging.info("%s: connected", _id) 75 buf = b"" 76 msg_expected = "handshake" 77 peer_name = None 78 while True: 79 # Read until we get Msg message {{{ 80 data = await reader.read(MaxMsgLen) 81 if data == b"": 82 logging.info("%s: closed connection", _id) 83 break 84 buf += data 85 if len(buf) > MaxMsgLen: 86 logging.warning("%s: max buffer size exceeded", _id) 87 break 88 try: 89 msg, tail = Msg().decode(buf) 90 except ASN1Error: 91 continue 92 buf = tail 93 # }}} 94 if msg.choice != msg_expected: 95 logging.warning("%s: unexpected %s message", _id, msg.choice) 96 break 97 if msg_expected == "text": 98 try: 99 await msg_receiver(msg.value, peer_name) 100 except ValueError as err: 101 logging.warning("%s: %s", err) 102 break 103 # Process Handshake message {{{ 104 elif msg_expected == "handshake": 105 logging.info("%s: got %s message", _id, msg_expected) 106 msg_handshake = msg.value 107 peer_name = str(msg_handshake["peerName"]) 108 if peer_name not in THEIR_NAMES: 109 logging.warning("unknown peer name: %s", peer_name) 110 break 111 writer.write(Msg(("handshake", MsgHandshake(( 112 ("peerName", OUR_NAME), 113 )))).encode()) 114 await writer.drain() 115 logging.info("%s: session established: %s", _id, peer_name) 116 peer_alive = PEER_ALIVES.pop(peer_name, None) 117 if peer_alive is not None: 118 peer_alive.close() 119 await IN_QUEUES[peer_name].put(None) 120 PEER_ALIVES[peer_name] = writer 121 asyncio.ensure_future(msg_sender(peer_name, writer)) 122 msg_expected = "text" 123 # }}} 124 logging.info("%s: disconnecting", _id) 125 if msg_expected == "text": 126 IN_QUEUES[peer_name].put(None) 127 writer.close() 

安全协议


是时候保证我们的沟通了。 我们所说的安全性是什么,我们想要什么:

  • 传输消息的机密性;
  • 传输消息的真实性和完整性-必须检测其更改;
  • 防止重放攻击-应该检测到消息丢失或重试的事实(我们决定断开连接);
  • 通过预先驱动的公钥识别和验证对话者-我们早先已经决定要建立一个朋友对朋友的网络。 只有经过身份验证,我们才能了解与谁进行通讯;
  • 完美的前向保密属性(PFS)的存在-我们长期存在的签名密钥的妥协不应该导致读取所有以前的对应关系的可能性。 记录拦截的流量变得无用;
  • 消息的有效性/有效性(传输和握手)仅在同一TCP会话内。 不可能从另一个会话(即使使用相同的对话者)插入经过正确签名/验证的消息;
  • 被动的观察者应该看不到用户标识符,传输的寿命很长的公钥,也不能从中获取哈希值。 被动观察者的某种匿名性。

令人惊讶的是,几乎每个人都希望在任何握手协议中都达到这一最低要求,而上述的极少数最终还是针对本地协议执行的。 所以现在我们不会发明新事物。 我绝对会建议使用Noise框架来构建协议,但让我们选择一些更简单的方法。

最受欢迎的是两种协议:

  • TLS是一个复杂的协议,具有悠久的错误,漏洞,漏洞,深思熟虑,复杂性和缺点的历史(但是,它不适用于TLS 1.3)。 但是由于复杂性,我们不考虑它。
  • 带有IKE的IPsec-尽管也不简单,但没有严重的密码问题。 如果您阅读有关IKEv1和IKEv2的信息,它们的来源是STS ,ISO / IEC IS 9798-3和SIGMA(SIGn-and-MAc)协议-足够容易在一个晚上实施。

SIGMA作为STS / ISO协议开发的最后一个环节,效果如何? 它满足我们的所有要求(包括“隐藏”对话者的标识符),没有已知的密码问题。 这非常简单-从协议消息中删除至少一个元素将导致其不安全。

让我们从最简单的本地协议过渡到SIGMA。 我们感兴趣的最基本的操作是键匹配 :这是一个函数,在该函数的输出中,两个参与者将获得相同的值,可用作对称键。 无需赘述:各方分别生成一个临时密钥(仅在同一会话中使用)密钥对(公共密钥和私有密钥),交换公共密钥,调用匹配功能,并向其输入传输其私有密钥和对话者的公共密钥。

 ┌─────┐┌─────┐
 │PeerA││PeerB│
 └──┬──┘└──┬──┘
    │IDA,PubA│I══╔╔╔
    │──────────────>>│rPrvA,PubA = DHgen()║
    ││╚═══════════════════╝
    │IdB,PubB│╔════════════════════╗
    │<─────────────│║PrvB,PubB = DHgen()║
    ││╚═══════════════════╝
    ────┐
        │║密钥= DH(PrvA,PubB)║
    <───┘
    ││
    ││


任何人都可以干预中间并用自己的密钥替换公钥-在此协议中,不会对对话者进行身份验证。 添加具有长寿命密钥的签名。

 ┌─────┐┌─────┐
 │PeerA││PeerB│
 └──┬──┘└──┬──┘
    │IdA,PubA,符号(SignPrvA,(PubA))│╔═══════════════════════╗
    │────ign ign ign─ign ign ign ign ign ign ign ign SignPrvA,SignPubA = load()║
    ││║PrvA,PubA = DHgen()║
    ││││══╝╝╝╝
    │IdB,PubB,符号(SignPrvB,(PubB))│╔═══════════════════════╗
    │<────────────────│ignSignPrvB,SignPubB = load()║
    ││║PrvB,PubB = DHgen()║
    ││││══╝╝╝╝
    ────┐╔═════════════════════││
        │║验证(SignPubB,...)││
    <───┘键= DH(PrvA,PubB)║│
    │╚═════════════════════╝│
    ││


这样的签名将不起作用,因为它没有绑定到特定的会话。 这样的消息也适合与其他参与者的会话。 整个上下文应该被订阅。 这也迫使添加来自A的另一个消息。

另外,添加您自己的标识符作为签名非常重要,因为否则,我们可以替换IdXXX,并使用另一个知名对话者的密钥对消息重新签名。 为了防止反射攻击 ,签名下的元素必须在含义上明确定义的位置:如果A签名(PubA,PubB),则B必须签名(PubB,PubA)。 这也表明选择序列化数据的结构和格式的重要性。 例如,对以ASN.1 DER编码的集合进行排序:SET OF(PubA,PubB)将与SET OF(PubB,PubA)相同。

 ┌─────┐┌─────┐
 │PeerA││PeerB│
 └──┬──┘└──┬──┘
    │IDA,PubA│╔══════════════════════════
    │────────ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign SignPubA =负载()║
    ││║PrvA,PubA = DHgen()║
    ││││══╝╝╝╝
    │IdB,PubB,符号(SignPrvB,(IdB,PubA,PubB))│╔═════════════════════╗
    │SignPrvB,│<────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── SignPubB =负载()║
    ││║PrvB,PubB = DHgen()║
    ││││══╝╝╝╝
    │签署(SignPrvA,(IdA,PubB,PubA))│╔═══════════════════╗
    │─────────────────── SignPubB,...)║
    ││║密钥= DH(PrvA,PubB)║
    ││││══╝╝╝
    ││


但是,我们仍然没有“证明”我们已经为此会话开发了相同的通用密钥。 原则上,您可以不执行此步骤-第一个传输连接将无效,但是我们希望在握手完成后,我们可以确保一切都已真正达成共识。 目前,我们已经掌握了ISO / IEC IS 9798-3协议。

我们可以自己签名密钥。 这很危险,因为所使用的签名算法可能会泄漏(每个签名保留位数,但仍会泄漏)。 您可以根据生成的密钥对哈希签名,但是即使生成密钥的哈希泄漏也可能对生成函数产生暴力攻击。 SIGMA使用MAC功能来验证发送方ID。

 ┌─────┐┌─────┐
 │PeerA││PeerB│
 └──┬──┘└──┬──┘
    │IDA,PubA│╔══════════════════════════
    │────────────────────────│──────│──││── >│║SignPrvA,SignPubA =负载()║
    ││║PrvA,PubA = DHgen()║
    ││││══╝╝╝╝
    │IdB,PubB,符号(SignPrvB,(PubA,PubB)),MAC(IdB)│╔═════════════════╗
    │<──────────────────────────││─── ─│SignPrvB,SignPubB = load()(
    ││║PrvB,PubB = DHgen()║
    ││││══╝╝╝╝
    ││││══╗╗╗
    │符号(SignPrvA,(PubB,PubA)),MAC(IdA)│║密钥= DH(PrvA,PubB)║
    │────────────────────────│──────│──││── >│║验证(密钥,以dB为单位)║
    ││║验证(SignPubB,...)║
    ││││══╝╝╝
    ││


作为一种优化,有些人可能想重用他们的临时密钥(当然,对于PFS来说,这是令人遗憾的)。 例如,我们生成了一个密钥对,尝试进行连接,但是TCP不可用或在协议中间某处断开。 遗憾的是将熵和处理器资源花费在新对上。 因此,我们引入了所谓的cookie-一个伪随机值,它将在重用短暂的公共密钥时防止可能的意外重放攻击。 由于cookie和临时公共密钥之间的绑定,可以在不需要时从签名中删除对方的公共密钥。

 ┌─────┐┌─────┐
 │PeerA││PeerB│
 └──┬──┘└──┬──┘
    │IDA,PubA,CookieA│╔═════════════════════════
    │────────────────────────│──────│──││── ────────────────────>>││SignPrvA,SignPubA = load()║
    ││║PrvA,PubA = DHgen()║
    ││││══╝╝╝╝
    │IdB,PubB,CookieB,符号(SignPrvB,(CookieA,CookieB,PubB)),MAC(IdB)│Id═══ ═══╗
    │<──────────────────────────────│││────────────── ────────────────────││SignPrvB,SignPubB = load()║
    ││║PrvB,PubB = DHgen()║
    ││││══╝╝╝╝
    ││││══╗╗╗
    │符号(SignPrvA,(CookieB,CookieA,PubA)),MAC(IdA)│║密钥= DH(PrvA,PubB)║
    │────────────────────────│──────│──││── ─验证验证验证(密钥,IdB)║
    ││║验证(SignPubB,...)║
    ││││══╝╝╝
    ││


最后,我们想从被动观察者那里获得对话者标识符的私密性。 为此,SIGMA建议先交换临时密钥,然后制定一个用于验证身份验证消息的公用密钥。 SIGMA描述了两个选项:

  • SIGMA-I-保护发起者免受主动攻击,保护响应者免受被动攻击:发起者对响应者进行身份验证,如果某些内容不合适,则不会给出其标识。 如果您与他开始主动协议,则被告将提供其身份证明。 被动的观察者将一无所知。
    SIGMA-R-保护响应者免受主动攻击,保护发起者免受被动攻击。 一切都完全相反,但是在此协议中,已经传输了四个握手消息。


    我们选择SIGMA-I的方式与我们通常从服务器-客户端中获得的结果更相似:只有经过身份验证的服务器才能识别客户端,而且每个人都知道服务器。 另外,由于减少了握手消息,因此更易于实现。 我们添加到协议中的就是消息部分的加密以及标识符A到最后一条消息的加密部分的传输:

     ┌─────┐┌─────┐
     │PeerA││PeerB│
     └──┬──┘└──┬──┘
        │PubA,CookieA│╔══════════════════════════╗
        │────────────────────────│──────│──││── ─ign ign ign ign ign ign ign ign ign ign ,,,,, SignPubA = load()║
        ││║PrvA,PubA = DHgen()║
       │ │ ╚═══════════════════════════╝
       │PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) │ ╔═══════════════════════════╗
       │<─────────────────────────────────────────────────────────────────────────────│ ║SignPrvB, SignPubB = load()║
       │ │ ║PrvB, PubB = DHgen() ║
       │ │ ╚═══════════════════════════╝
       │ │ ╔═════════════════════╗
       │ Enc((IdA, sign(SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║Key = DH(PrvA, PubB) ║
       │─────────────────────────────────────────────────────────────────────────────>│ ║verify(Key, IdB) ║
       │ │ ║verify(SignPubB, ...)║
       │ │ ╚═════════════════════╝
       │ │
    


    • 34.10-2012 256- .
    • 34.10-2012 VKO.
    • MAC CMAC. , 34.13-2015. — (34.12-2015).
    • . -256 (34.11-2012 256 ).


    . . : , , (MAC) , . , , . , , ? . , KDF (key derivation function). , - : HKDF , . , Python , hkdf . HKDF HMAC , , , -. Python Wikipedia . 34.10-2012, - -256. , :

     kdf = Hkdf(None, key_session, hash=GOST34112012256) kdf.expand(b"handshake1-mac-identity") kdf.expand(b"handshake1-enc") kdf.expand(b"handshake1-mac") kdf.expand(b"handshake2-mac-identity") kdf.expand(b"handshake2-enc") kdf.expand(b"handshake2-mac") kdf.expand(b"transport-initiator-enc") kdf.expand(b"transport-initiator-mac") kdf.expand(b"transport-responder-enc") kdf.expand(b"transport-responder-mac") 

    /


    ASN.1 :

     class Msg(Choice): schema = (( ("text", MsgText()), ("handshake0", MsgHandshake0(expl=tag_ctxc(0))), ("handshake1", MsgHandshake1(expl=tag_ctxc(1))), ("handshake2", MsgHandshake2(expl=tag_ctxc(2))), )) class MsgText(Sequence): schema = (( ("payload", MsgTextPayload()), ("payloadMac", MAC()), )) class MsgTextPayload(Sequence): schema = (( ("nonce", Integer(bounds=(0, float("+inf")))), ("ciphertext", OctetString(bounds=(1, MaxTextLen))), )) class MsgHandshake0(Sequence): schema = (( ("cookieInitiator", Cookie()), ("pubKeyInitiator", PubKey()), )) class MsgHandshake1(Sequence): schema = (( ("cookieResponder", Cookie()), ("pubKeyResponder", PubKey()), ("ukm", OctetString(bounds=(8, 8))), ("ciphertext", OctetString()), ("ciphertextMac", MAC()), )) class MsgHandshake2(Sequence): schema = (( ("ciphertext", OctetString()), ("ciphertextMac", MAC()), )) class HandshakeTBE(Sequence): schema = (( ("identity", OctetString(bounds=(32, 32))), ("signature", OctetString(bounds=(64, 64))), ("identityMac", MAC()), )) class HandshakeTBS(Sequence): schema = (( ("cookieTheir", Cookie()), ("cookieOur", Cookie()), ("pubKeyOur", PubKey()), )) class Cookie(OctetString): bounds = (16, 16) class PubKey(OctetString): bounds = (64, 64) class MAC(OctetString): bounds = (16, 16) 

    HandshakeTBS — , (to be signed). HandshakeTBE — , (to be encrypted). ukm MsgHandshake1. 34.10 VKO, , UKM (user keying material) — .


    , ( , , ).

    , - . JSON :

     { "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } } 

    our — , . their — . JSON :

     from pygost import gost3410 from pygost.gost34112012256 import GOST34112012256 CURVE = gost3410.GOST3410Curve( *gost3410.CURVE_PARAMS["GostR3410_2001_CryptoPro_A_ParamSet"] ) parser = argparse.ArgumentParser(description="GOSTIM") parser.add_argument( "--keys-gen", action="store_true", help="Generate JSON with our new keypair", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON with our and their keys", ) parser.add_argument( "--bind", default="::1", help="Address to listen on", ) parser.add_argument( "--port", type=int, default=6666, help="Port to listen on", ) args = parser.parse_args() if args.keys_gen: prv_raw = urandom(32) pub = gost3410.public_key(CURVE, gost3410.prv_unmarshal(prv_raw)) pub_raw = gost3410.pub_marshal(pub) print(json.dumps({ "our": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)}, "their": {}, })) exit(0) # Parse and unmarshal our and their keys {{{ with open(args.keys, "rb") as fd: _keys = json.loads(fd.read().decode("utf-8")) KEY_OUR_SIGN_PRV = gost3410.prv_unmarshal(hexdec(_keys["our"]["prv"])) _pub = hexdec(_keys["our"]["pub"]) KEY_OUR_SIGN_PUB = gost3410.pub_unmarshal(_pub) KEY_OUR_SIGN_PUB_HASH = OctetString(GOST34112012256(_pub).digest()) for peer_name, pub_raw in _keys["their"].items(): _pub = hexdec(pub_raw) KEYS[GOST34112012256(_pub).digest()] = { "name": peer_name, "pub": gost3410.pub_unmarshal(_pub), } # }}} 

    34.10 — . 256- 256- . PyGOST , , (urandom(32)) , gost3410.prv_unmarshal(). , gost3410.public_key(). 34.10 — , , gost3410.pub_marshal().

    JSON , , , , gost3410.pub_unmarshal(). , . -256 gost34112012256.GOST34112012256(), hashlib -.

    ? , : cookie (128- ), 34.10, VKO .

      395 async def initiator(host, port): 396 _id = repr((host, port)) 397 logging.info("%s: dialing", _id) 398 reader, writer = await asyncio.open_connection(host, port) 399 # Generate our ephemeral public key and cookie, send Handshake 0 message {{{ 400 cookie_our = Cookie(urandom(16)) 401 prv = gost3410.prv_unmarshal(urandom(32)) 402 pub_our = gost3410.public_key(CURVE, prv) 403 pub_our_raw = PubKey(gost3410.pub_marshal(pub_our)) 404 writer.write(Msg(("handshake0", MsgHandshake0(( 405 ("cookieInitiator", cookie_our), 406 ("pubKeyInitiator", pub_our_raw), 407 )))).encode()) 408 # }}} 409 await writer.drain() 

    • Msg ;
    • handshake1;
    • ;
    • TBE .

      423 logging.info("%s: got %s message", _id, msg.choice) 424 if msg.choice != "handshake1": 425 logging.warning("%s: unexpected message, disconnecting", _id) 426 writer.close() 427 return 428 # }}} 429 msg_handshake1 = msg.value 430 # Validate Handshake message {{{ 431 cookie_their = msg_handshake1["cookieResponder"] 432 pub_their_raw = msg_handshake1["pubKeyResponder"] 433 pub_their = gost3410.pub_unmarshal(bytes(pub_their_raw)) 434 ukm_raw = bytes(msg_handshake1["ukm"]) 435 ukm = ukm_unmarshal(ukm_raw) 436 key_session = kek_34102012256(CURVE, prv, pub_their, ukm, mode=2001) 437 kdf = Hkdf(None, key_session, hash=GOST34112012256) 438 key_handshake1_mac_identity = kdf.expand(b"handshake1-mac-identity") 439 key_handshake1_enc = kdf.expand(b"handshake1-enc") 440 key_handshake1_mac = kdf.expand(b"handshake1-mac") 

    UKM 64- (urandom(8)), , gost3410_vko.ukm_unmarshal(). VKO 34.10-2012 256- gost3410_vko.kek_34102012256() (KEK — key encryption key).

    256- . HKDF . GOST34112012256 hashlib , Hkdf . ( Hkdf) , - . kdf.expand() 256-, .

    TBE TBS :

    • MAC ;
    • ;
    • TBE ;
    • ;
    • MAC ;
    • TBS , cookie . .

      441 try: 442 peer_name = validate_tbe( 443 msg_handshake1, 444 key_handshake1_mac_identity, 445 key_handshake1_enc, 446 key_handshake1_mac, 447 cookie_our, 448 cookie_their, 449 pub_their_raw, 450 ) 451 except ValueError as err: 452 logging.warning("%s: %s, disconnecting", _id, err) 453 writer.close() 454 return 455 # }}} 128 def validate_tbe( 129 msg_handshake: Union[MsgHandshake1, MsgHandshake2], 130 key_mac_identity: bytes, 131 key_enc: bytes, 132 key_mac: bytes, 133 cookie_their: Cookie, 134 cookie_our: Cookie, 135 pub_key_our: PubKey, 136 ) -> str: 137 ciphertext = bytes(msg_handshake["ciphertext"]) 138 mac_tag = mac(GOST3412Kuznechik(key_mac).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext) 139 if not compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])): 140 raise ValueError("invalid MAC") 141 plaintext = ctr( 142 GOST3412Kuznechik(key_enc).encrypt, 143 KUZNECHIK_BLOCKSIZE, 144 ciphertext, 145 8 * b"\x00", 146 ) 147 try: 148 tbe, _ = HandshakeTBE().decode(plaintext) 149 except ASN1Error: 150 raise ValueError("can not decode TBE") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 peer = KEYS.get(key_sign_pub_hash) 153 if peer is None: 154 raise ValueError("unknown identity") 155 mac_tag = mac( 156 GOST3412Kuznechik(key_mac_identity).encrypt, 157 KUZNECHIK_BLOCKSIZE, 158 key_sign_pub_hash, 159 ) 160 if not compare_digest(mac_tag, bytes(tbe["identityMac"])): 161 raise ValueError("invalid identity MAC") 162 tbs = HandshakeTBS(( 163 ("cookieTheir", cookie_their), 164 ("cookieOur", cookie_our), 165 ("pubKeyOur", pub_key_our), 166 )) 167 if not gost3410.verify( 168 CURVE, 169 peer["pub"], 170 GOST34112012256(tbs.encode()).digest(), 171 bytes(tbe["signature"]), 172 ): 173 raise ValueError("invalid signature") 174 return peer["name"] 

    , 34.13-2015 34.12-2015. , MAC-. PyGOST gost3413.mac(). ( ), , , . hardcode- ? 34.12-2015 128- , 64- — 28147-89, .

    gost.3412.GOST3412Kuznechik(key) .encrypt()/.decrypt() , 34.13 . MAC : gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). MAC- (==) , , , , BEAST TLS. Python hmac.compare_digest .

    . , , . 34.13-2015 : ECB, CTR, OFB, CBC, CFB. . , ( CCM, OCB, GCM ) — MAC. (CTR): , , , ( CBC, ).

    .mac(), .ctr() : ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). , . ( ), . handshake .

    gost3410.verify() : ( GOSTIM ), ( , , ), 34.11-2012 .

    , handshake2 , , : , .…

      456 # Prepare and send Handshake 2 message {{{ 457 tbs = HandshakeTBS(( 458 ("cookieTheir", cookie_their), 459 ("cookieOur", cookie_our), 460 ("pubKeyOur", pub_our_raw), 461 )) 462 signature = gost3410.sign( 463 CURVE, 464 KEY_OUR_SIGN_PRV, 465 GOST34112012256(tbs.encode()).digest(), 466 ) 467 key_handshake2_mac_identity = kdf.expand(b"handshake2-mac-identity") 468 mac_tag = mac( 469 GOST3412Kuznechik(key_handshake2_mac_identity).encrypt, 470 KUZNECHIK_BLOCKSIZE, 471 bytes(KEY_OUR_SIGN_PUB_HASH), 472 ) 473 tbe = HandshakeTBE(( 474 ("identity", KEY_OUR_SIGN_PUB_HASH), 475 ("signature", OctetString(signature)), 476 ("identityMac", MAC(mac_tag)), 477 )) 478 tbe_raw = tbe.encode() 479 key_handshake2_enc = kdf.expand(b"handshake2-enc") 480 key_handshake2_mac = kdf.expand(b"handshake2-mac") 481 ciphertext = ctr( 482 GOST3412Kuznechik(key_handshake2_enc).encrypt, 483 KUZNECHIK_BLOCKSIZE, 484 tbe_raw, 485 8 * b"\x00", 486 ) 487 mac_tag = mac( 488 GOST3412Kuznechik(key_handshake2_mac).encrypt, 489 KUZNECHIK_BLOCKSIZE, 490 ciphertext, 491 ) 492 writer.write(Msg(("handshake2", MsgHandshake2(( 493 ("ciphertext", OctetString(ciphertext)), 494 ("ciphertextMac", MAC(mac_tag)), 495 )))).encode()) 496 # }}} 497 await writer.drain() 498 logging.info("%s: session established: %s", _id, peer_name) 

    , ( , , ), MAC-:

      499 # Run text message sender, initialize transport decoder {{{ 500 key_initiator_enc = kdf.expand(b"transport-initiator-enc") 501 key_initiator_mac = kdf.expand(b"transport-initiator-mac") 502 key_responder_enc = kdf.expand(b"transport-responder-enc") 503 key_responder_mac = kdf.expand(b"transport-responder-mac") ... 509 asyncio.ensure_future(msg_sender( 510 peer_name, 511 key_initiator_enc, 512 key_initiator_mac, 513 writer, 514 )) 515 encrypter = GOST3412Kuznechik(key_responder_enc).encrypt 516 macer = GOST3412Kuznechik(key_responder_mac).encrypt 517 # }}} 519 nonce_expected = 0 520 # Wait for test messages {{{ 521 while True: 522 data = await reader.read(MaxMsgLen) ... 530 msg, tail = Msg().decode(buf) ... 537 try: 538 await msg_receiver( 539 msg.value, 540 nonce_expected, 541 macer, 542 encrypter, 543 peer_name, 544 ) 545 except ValueError as err: 546 logging.warning("%s: %s", err) 547 break 548 nonce_expected += 1 549 # }}} 

    msg_sender , TCP-. nonce, . .

     async def msg_sender(peer_name: str, key_enc: bytes, key_mac: bytes, writer) -> None: nonce = 0 encrypter = GOST3412Kuznechik(key_enc).encrypt macer = GOST3412Kuznechik(key_mac).encrypt in_queue = IN_QUEUES[peer_name] while True: text = await in_queue.get() if text is None: break ciphertext = ctr( encrypter, KUZNECHIK_BLOCKSIZE, text.encode("utf-8"), long2bytes(nonce, 8), ) payload = MsgTextPayload(( ("nonce", Integer(nonce)), ("ciphertext", OctetString(ciphertext)), )) mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) writer.write(Msg(("text", MsgText(( ("payload", payload), ("payloadMac", MAC(mac_tag)), )))).encode()) nonce += 1 

    msg_receiver, :

     async def msg_receiver( msg_text: MsgText, nonce_expected: int, macer, encrypter, peer_name: str, ) -> None: payload = msg_text["payload"] if int(payload["nonce"]) != nonce_expected: raise ValueError("unexpected nonce value") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("invalid MAC") plaintext = ctr( encrypter, KUZNECHIK_BLOCKSIZE, bytes(payload["ciphertext"]), long2bytes(nonce_expected, 8), ) text = plaintext.decode("utf-8") await OUT_QUEUES[peer_name].put(text) 

    结论


    GOSTIM ( , )! (-256 : 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). , GoGOST , PyDERASN , NNCP , GoVPN , GOSTIM , GPLv3+ .

    , , , Python/Go-, « „“ .

Source: https://habr.com/ru/post/zh-CN452200/


All Articles