Als ontwikkelaar van de bibliotheek (GOST-cryptografische primitieve in pure Python), krijg ik vaak vragen over hoe je eenvoudig een veilige berichtenuitwisseling kunt implementeren. Velen beschouwen toegepaste cryptografie als iets eenvoudigs, en een aanroep van .encrypt() bij een blokversleuteling zou voldoende zijn voor veilige verzending via een communicatiekanaal. Anderen denken echter dat toegepaste cryptografie iets is voor weinigen, en dat het acceptabel is dat rijke bedrijven zoals Telegram met wiskundigen van Olympische wedstrijden Dit alles heeft me ertoe aangezet om dit artikel te schrijven, om te laten zien dat het implementeren van cryptografische protocollen en veilige IM niet zo'n complexe taak is. Het is echter niet verstandig om je eigen authenticatie- en sleutelovereenstemmingprotocollen uit te vinden.
In het artikel wordt

peer-to-peer , , SIGMA-I IPsec IKE PyDERASN heb geschreven Het ontwerp van IM
Om te beginnen moeten we begrijpen hoe onze IM eruit zal zien. Voor de eenvoud laten we dit een peer-to-peer netwerk zijn, zonder enige deelnemerdetectie. We zullen zelf aangeven naar welk adres: poort we moeten verbinden om met de gespreksgenoot te communiceren.
Ik begrijp dat, op dit moment, de veronderstelling van beschikbare directe verbinding tussen twee willekeurige computers een aanzienlijke beperking is voor de praktische toepasbaarheid van IM. Maar hoe meer ontwikkelaars allerlei NAT-traversal oplossingen implementeren, hoe langer we in het IPv4-internet zullen blijven, met teleurstellende kansen voor verbinding tussen willekeurige computers. Hoe lang kunnen we nog het ontbreken van IPv6 thuis en op het werk verdragen?
Ik begrijp dat de veronderstelling van directe verbinding tussen twee willekeurige computers op dit moment een aanzienlijke beperking vormt voor de toepasbaarheid van IM in de praktijk. Maar hoe meer ontwikkelaars allerlei NAT-traversal oplossingen implementeren, hoe langer we in het IPv4-Internet zullen blijven, met een weinig hoopgevende kans op verbinding tussen willekeurige computers. Hoe lang kunnen we het gebrek aan IPv6 thuis en op het werk nog verdragen?
We will have a friend-to-friend network: all possible interlocutors must be known in advance. First, this simplifies everything significantly: we introduce ourselves, find or do not find the name/key, disconnect, or continue working, knowing the interlocutor. Second, in general, this is safe and excludes many attacks.
The IM interface will be close to classic solutions. , which I really like for their minimalism and Unix way philosophy. The IM program creates a directory for each interlocutor with three Unix domain sockets:
- in β this is where the messages sent to the interlocutor are recorded;
- out β this is where the messages received from the interlocutor are read;
- state β by reading this, we find out whether the interlocutor is currently connected, the address/port of the connection.
In addition, a conn socket is created, and by writing the host port into it, we initiate a connection to the remote interlocutor.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
This approach allows for independent implementations of the IM transport and user interface, as there is no accounting for taste; you can't please everyone. By using and/or , one can achieve a multi-window interface with syntax highlighting. And with the help of , one can get a GNU Readline-compatible line for message input.
In fact, suckless projects use FIFO files. Personally, I couldnβt figure out how to work with files concurrently in asyncio without a custom layer of dedicated threads (I've been using the language for such things for a long time ). So I decided to stick with Unix domain sockets. Unfortunately, this eliminates the possibility of doing echo 2001:470:dead::babe 6666 > conn. I solved this problem by using : echo 2001:470:dead::babe 6666 | socat β UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
The initial unsafe protocol
TCP is used as the transport: it guarantees delivery and its order. UDP guarantees neither (which would be useful when applying cryptography), and there is no support for in Python out of the box.
Helaas bestaat er in TCP geen begrip van een bericht, alleen van een stroom bytes. Daarom moeten we een formaat voor berichten bedenken, zodat ze in deze stroom van elkaar kunnen worden gescheiden. We kunnen afspreken het nieuwe regelteken te gebruiken. In het begin is dat voldoende, maar wanneer we onze berichten gaan versleutelen, kan dit teken overal in de ciphertext verschijnen. In netwerken zijn daarom protocollen populair die eerst de lengte van het bericht in bytes verzenden. Bijvoorbeeld, in Python biedt de xdrlib standaard ondersteuning voor een dergelijk formaat. .
We zullen niet goed en efficiΓ«nt werken met TCP-lezingen β laten we de code vereenvoudigen. We lezen gegevens uit de socket in een oneindige lus totdat we het volledige bericht decoderen. Voor een dergelijke aanpak kunnen ook JSON en XML als formaat worden gebruikt. Maar wanneer cryptografie wordt toegevoegd, moeten de gegevens worden ondertekend en geverifieerd β en dat vereist een byte-voor-byte identieke representatie van objecten, wat niet wordt gegarandeerd door JSON/XML (de dumps kunnen verschillen).
XDR is geschikt voor deze taak, echter kies ik voor ASN.1 met DER-codering en de bibliotheek, aangezien we zullen werken met hoge-level objecten die vaak prettiger en handiger zijn om mee te werken. In tegenstelling tot schemaloze , of , ASN.1 controleert automatisch de gegevens aan de hand van een strikt gedefinieerde schema.
# 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))),
))
Het ontvangen bericht zal Msg zijn: of een tekstbericht MsgText (tot nu toe met één tekstveld), of een handshaking-bericht MsgHandshake (waarbij de naam van de gesprekspartner wordt doorgegeven). Het ziet er nu misschien overgecompliceerd uit, maar dit is een voorbereiding voor de toekomst.
βββββββ βββββββ
βPeerAβ βPeerBβ
ββββ¬βββ ββββ¬βββ
βMsgHandshake(IdA) β
ββββββββββββββββββ>β
β β
βMsgHandshake(IdB) β
ββ
β β
β MsgText() β
β<ββββββββββββββββββ
β β
IM zonder cryptografie
Zoals ik al zei, zal de asyncio-bibliotheek voor alle socketbewerkingen worden gebruikt. Laten we verklaren wat we verwachten op het moment van uitvoering:
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Onze peer-naam",
)
parser.add_argument(
"--their-names",
required=True,
help="Hun peer-namen, gescheiden door komma's",
)
parser.add_argument(
"--bind",
default="::1",
help="Adres om op te luisteren",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Poort om op te luisteren",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Stel de eigen naam in (βour-name alice). Vermeld alle verwachte gesprekspartners door een komma (βtheir-names bob,eve). Voor elke gesprekspartner wordt er een directory aangemaakt met Unix-sockets, evenals een coroutine voor elke in, uit, toestand:
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"))
Berichten van de gebruiker die via de in-socket binnenkomen, worden naar de IN_QUEUES-queues gestuurd:
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"))
Berichten van gesprekspartners worden naar de OUT_QUEUES-queues gestuurd, waarvan de gegevens in de out-socket worden geschreven:
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()
Bij het lezen van de state-socket zoekt het programma in de PEER_ALIVE-woordenlijst naar het adres van de gesprekspartner. Als er nog geen verbinding met de gesprekspartner is, wordt er een lege string geschreven.
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()
Bij het schrijven van het adres naar de conn-socket wordt de functie "initiator" voor de verbinding gestart:
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))
Laten we de initiator bekijken. Eerst opent hij, uiteraard, een verbinding met het opgegeven host/poort en verzendt een handshake-bericht met zijn naam:
130 async def initiator(host, port):
131 _id = repr((host, port))
132 logging.info("%s: aan het bellen", _id)
133 reader, writer = await asyncio.open_connection(host, port)
134 # Handshake bericht {{{
135 writer.write(Msg(("handshake", MsgHandshake((
136 ("peerName", OUR_NAME),
137 )))).encode())
138 # }}}
139 await writer.drain()
Vervolgens wacht het op een antwoord van de externe zijde. Het probeert het ontvangen antwoord te decoderen volgens het Msg ASN.1 schema. We veronderstellen dat het hele bericht in één TCP-segment wordt verzonden en we het atomair ontvangen bij het aanroepen van .read(). We controleren of we inderdaad het handshake-bericht hebben ontvangen.
141 # Wacht op Handshake bericht {{{
142 data = await reader.read(256)
143 if data == b"":
144 logging.warning("%s: geen antwoord, verbreken", _id)
145 writer.close()
146 return
147 try:
148 msg, _ = Msg().decode(data)
149 except ASN1Error:
150 logging.warning("%s: ondecodeerbaar antwoord, verbreken", _id)
151 writer.close()
152 return
153 logging.info("%s: kreeg %s bericht", _id, msg.choice)
154 if msg.choice != "handshake":
155 logging.warning("%s: onverwacht bericht, verbreken", _id)
156 writer.close()
157 return
158 # }}}
We controleren of de ontvangen naam van de gesprekspartner bekend is. Als dat niet het geval is, verbreken we de verbinding. We controleren of we al een verbinding met hem hebben gehad (de gesprekspartner heeft opnieuw opdracht gegeven om verbinding met ons te maken) en sluiten deze. In IN_QUEUES worden Python-strings met de tekst van het bericht geplaatst, maar er is een speciale waarde None, die signaliseert dat de msg_sender coroutine moet stoppen, zodat deze haar writer die is verbonden met de verouderde TCP-verbinding vergeet.
159 msg_handshake = msg.value
160 peer_name = str(msg_handshake["peerName"])
161 if peer_name not in THEIR_NAMES:
162 logging.warning("onbekende peer naam: %s", peer_name)
163 writer.close()
164 return
165 logging.info("%s: sessie tot stand gebracht: %s", _id, peer_name)
166 # Start tekstbericht verzender, initialiseert transportdecoder {{{
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 accepteert uitgaande berichten (geplaatst in de wachtrij vanuit de in socket), serializeert deze naar een MsgText bericht en verzendt dit via de TCP-verbinding. Deze kan op elk moment worden onderbroken β dit vangen we expliciet op.
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: verzonden %d tekens bericht", peer_name, len(text))
Aan het einde komt de initiator in een oneindige lus terecht waarin berichten uit de socket worden gelezen. Hij controleert of dit tekstberichten zijn en plaatst ze in de OUT_QUEUES, de wachtrij waaruit ze naar de out-socket van de betreffende gesprekspartner zullen worden verzonden. Waarom kunnen we niet gewoon .read() aanroepen en het bericht decoderen? Omdat het mogelijk is dat meerdere berichten van de gebruiker in de buffer van het besturingssysteem worden samengevoegd en als één TCP-segment worden verzonden. We kunnen het eerste decoderen, maar er kan een deel van het volgende bericht in de buffer achterblijven. Bij elke onvoorziene situatie sluiten we de TCP-verbinding en stoppen we de msg_sender coroutine (met None in de OUT_QUEUES-wachtrij).
174 buf = b""
175 # Wacht op testberichten {{{
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: maximale buffergrootte overschreden", _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: onverwacht %s bericht", _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: verbindt af: %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: ontvangen %d tekens bericht", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
Laten we terugkeren naar de hoofdcode. Na het creΓ«ren van alle coroutines bij het opstarten van het programma starten we de TCP-server. Voor elke tot stand gekomen verbinding creΓ«ert hij een responder coroutine.
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("Luistert op: %s", server.sockets[0].getsockname())
loop.run_forever()
De responder is vergelijkbaar met de initiator en voert spiegelt de precies dezelfde acties uit, maar de oneindige lus voor het lezen van berichten wordt onmiddellijk gestart, voor de eenvoud. Momenteel verzendt het handshake-protocol één bericht van elke kant, maar in de toekomst zal het twee berichten van de initiator zijn, waarna het onmiddellijk mogelijk is om tekstberichten te verzenden.
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()
Veilig protocol
Het is tijd om onze communicatie veilig te stellen. Wat bedoelen we precies met veiligheid en wat willen we:
- de vertrouwelijkheid van de verzonden berichten;
- de authenticiteit en integriteit van de verzonden berichten - wijzigingen moeten worden gedetecteerd;
- bescherming tegen afspeelaanvallen (replay attack) - het verlies of herhaling van berichten moet worden gedetecteerd (en we besluiten de verbinding te verbreken);
- identificatie en authenticatie van gesprekspartners aan de hand van vooraf ingevoerde openbare sleutels - we hebben eerder besloten een friend-to-friend netwerk te maken. Pas na authenticatie begrijpen we met wie we communiceren;
- aanwezigheid eigenschappen (PFS) - de compromittering van onze langdurige handtekening sleutel mag niet leiden tot de mogelijkheid om eerdere correspondentie te lezen. Opname van onderschepte gegevens wordt nutteloos.
- De geldigheid/validiteit van berichten (transport en handdruk) is alleen binnen één TCP-sessie. Het invoegen van correct ondertekende/geauthenticeerde berichten uit een andere sessie (zelfs met dezelfde gesprekspartner) zou niet mogelijk moeten zijn;
- een passieve waarnemer mag noch gebruikersidentificaties, noch verzonden langlevende openbare sleutels, noch hashes daarvan zien. Een zekere anonimiteit tegenover een passieve waarnemer.
Het is verbazingwekkend, maar dit minimum willen bijna alle protocollen voor handdruk hebben, en heel weinig van het genoemde wordt uiteindelijk gerealiseerd voor 'in-house' protocollen. Laten we nu ook niets nieuws uitvinden. Ik zou zeker aanbevelen om te gebruiken bij het opbouwen van protocollen, maar laten we iets eenvoudigers kiezen.
De meest populaire zijn twee protocollen:
- β een complexe protocol met een lange geschiedenis van bugs, fouten, kwetsbaarheden, slecht doordachte aspecten, complexiteit en tekortkomingen (hoewel dit niet veel van toepassing is op TLS 1.3). Maar we beschouwen het niet vanwege de overcomplexiteit.
- met β hebben geen serieuze cryptografische problemen, hoewel ze ook niet eenvoudig zijn. Als je over IKEv1 en IKEv2 leest, zijn hun oorsprongen , ISO/IEC IS 9798-3 en SIGMA (SIGn-and-MAc) protocollen β ze zijn vrij eenvoudig te implementeren in één avond.
Wat maakt SIGMA, als laatste schakel in de ontwikkeling van STS/ISO-protocollen, goed? Het voldoet aan al onze vereisten (inclusief het 'verbergen' van identificaties van gesprekspartners), en heeft geen bekende cryptografische problemen. Het is minimalistisch β het verwijderen van zelfs maar één element uit het protocolbericht zou de veiligheid in gevaar brengen.
Laten we van het eenvoudigste in-house protocol naar SIGMA gaan. De meest basale operatie die ons interesseert is : een functie waarbij beide deelnemers dezelfde waarde ontvangen die als symmetrische sleutel kan worden gebruikt. Zonder in details te treden: elke partij genereert een ephemeral (alleen binnen één sessie gebruikte) sleutelpair (openbare en private sleutels), wisselt publieke sleutels uit, roept de sleutelovereenkomstfunctie aan waarbij ze hun private sleutel en de publieke sleutel van de gesprekspartner invoeren.
βββββββ βββββββ
βPeerAβ βPeerBβ
ββββ¬βββ ββββ¬βββ
β IdA, PubA β ββββββββββββββββββββββ
βββββββββββββββββΊβ βPrvA, PubA = DHgen()β
β β ββββββββββββββββββββββ
β IdB, PubB β ββββββββββββββββββββββ
ββββββββββββββββββ βPrvB, PubB = DHgen()β
β β ββββββββββββββββββββββ
βββββ βββββββββ§βββββββββββββ
β βKey = DH(PrvA, PubB)β
ββββββ βββββββββ€βββββββββββββ
β β
β β
Iedereen kan zich tussenbegeven en publieke sleutels met hun eigen sleutels vervangen - dit protocol heeft geen authenticatie van gesprekspartners. We voegen een handtekening toe met langlevende sleutels.
βββββββ βββββββ
βPeerAβ βPeerBβ
ββββ¬βββ ββββ¬βββ
βIdA, PubA, sign(SignPrvA, (PubA)) β βββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββΊβ βSignPrvA, SignPubA = load()β
β β βPrvA, PubA = DHgen() β
β β βββββββββββββββββββββββββββββ
βIdB, PubB, sign(SignPrvB, (PubB)) β βββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ βSignPrvB, SignPubB = load()β
β β βPrvB, PubB = DHgen() β
β β βββββββββββββββββββββββββββββ
βββββ βββββββββββββββββββββββ β
β βverify(SignPubB, ...)β β
ββββββ βKey = DH(PrvA, PubB) β β
β βββββββββββββββββββββββ β
β β
Zo'n handtekening voldoet niet, omdat deze niet aan een specifieke sessie is gekoppeld. Dergelijke berichten "zullen" ook geschikt zijn voor sessies met andere deelnemers. De hele context moet handtekeningen bevatten. Dit dwingt ook tot het toevoegen van een extra bericht van A.
Daarnaast is het cruciaal om ook je eigen identificator aan de handtekening toe te voegen, anders kunnen we IdXXX vervalsen en het bericht herschrijven met de sleutel van een andere bekende gesprekspartner. Om te voorkomen dat , het is noodzakelijk dat de elementen onder de opmerking zich op duidelijk gedefinieerde plaatsen bevinden volgens hun betekenis: als A ondertekent (PubA, PubB), moet B ondertekenen (PubB, PubA). Dit benadrukt ook het belang van het kiezen van de structuur en het formaat van de geserialiseerde gegevens. Bijvoorbeeld, verzamelingen in ASN.1 DER-codering worden gesorteerd: SET OF(PubA, PubB) zal identiek zijn aan SET OF(PubB, PubA).
βββββββ βββββββ βPeerAβ βPeerBβ ββββ¬βββ ββββ¬βββ β IdA, PubA β βββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββΆβ βSignPrvA, SignPubA = load()β β β βPrvA, PubA = DHgen() β β β βββββββββββββββββββββββββββββ βIdB, PubB, sign(SignPrvB, (IdB, PubA, PubB)) β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββ βSignPrvB, SignPubB = load()β β β βPrvB, PubB = DHgen() β β β βββββββββββββββββββββββββββββ β sign(SignPrvA, (IdA, PubB, PubA)) β βββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββΆβ βverify(SignPubB, ...)β β β βKey = DH(PrvA, PubB) β β β βββββββββββββββββββββββ β β
Echter, we hebben nog steeds niet 'bewezen' dat we dezelfde gemeenschappelijke sleutel voor deze sessie hebben geproduceerd. In principe zou deze stap overgeslagen kunnen worden β het eerste transportbericht zou ongeldig zijn, maar we willen dat wanneer de handshake is voltooid, we zeker weten dat alles daadwerkelijk is overeengekomen. Op dit moment hebben we de ISO/IEC IS 9798-3 protocol in handen.
We zouden ook de geproduceerde sleutel zelf kunnen ondertekenen. Dit is gevaarlijk, omdat het niet uitgesloten is dat er lekken in het gebruikte handtekeningalgoritme kunnen zijn (hoewel het gaat om bits-voor-handtekening, het blijven lekken). We kunnen een hash van de geproduceerde sleutel ondertekenen, maar een lek van zelfs de hash van de geproduceerde sleutel kan waarde hebben bij brute-force aanvallen op de generatiefunctie. SIGMA maakt gebruik van een MAC-functie die de identiteit van de afzender authenticeert.
βββββββ βββββββ βPeerAβ βPeerBβ ββββ¬βββ ββββ¬βββ β IdA, PubA β βββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββββββ>>β βSignPrvA, SignPubA = load()β β β βPrvA, PubA = DHgen() β β β βββββββββββββββββββββββββββββ βIdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) β βββββββββββββββββββββββββββββ β<ββββββββββββββββββββββββββββββββββββββββββββββββββ βSignPrvB, SignPubB = load()β β β βPrvB, PubB = DHgen() β β β βββββββββββββββββββββββββββββ β β βββββββββββββββββββββββ β sign(SignPrvA, (PubB, PubA)), MAC(IdA) β βKey = DH(PrvA, PubB) β ββββββββββββββββββββββββββββββββββββββββββββββββββ>>β βverify(Key, IdB) β β β βverify(SignPubB, ...)β β β βββββββββββββββββββββββ β β
Als optimalisatie willen sommigen misschien hun ephemeral sleutels hergebruiken (wat uiteraard slecht is voor PFS). Bijvoorbeeld, we hebben een sleutelpaar gegenereerd en geprobeerd verbinding te maken, maar TCP was niet beschikbaar of was ergens halverwege het protocol verbroken. Het zou zonde zijn om de verbruikte entropie en CPU-bronnen te verspillen aan een nieuw paar. Daarom introduceren we het zogenaamde cookie β een pseudo-willekeurige waarde die zou beschermen tegen mogelijke replay-aanvallen bij hergebruik van ephemeral publieke sleutels. Vanwege de binding tussen het cookie en de ephemeral publieke sleutel kan de publieke sleutel van de tegenpartij uit de handtekening worden weggelaten als deze niet nodig is.
βββββββ βββββββ βPeerAβ βPeerBβ ββββ¬βββ ββββ¬βββ β IdA, PubA, CookieA β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ>β βSignPrvA, SignPubA = load()β β β βPrvA, PubA = DHgen() β β β βββββββββββββββββββββββββββββ βIdB, PubB, CookieB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) β βββββββββββββββββββββββββββββ ββ βverify(Key, IdB) β β β βverify(SignPubB, ...)β β β βββββββββββββββββββββββ β β
Ten slotte willen we de privacy van onze gesprekspartners beschermen tegen passieve waarnemers. Hiervoor stelt SIGMA voor om eerst ephemerale sleutels uit te wisselen, een gedeelde sleutel te genereren waarmee authenticerende en identificerende berichten worden versleuteld. SIGMA beschrijft twee varianten:
- SIGMA-I β beschermt de initiator tegen actieve aanvallen, de ontvanger tegen passieve: de initiator authenticeert de ontvanger en als er iets niet klopt, onthult hij zijn identificatie niet. De ontvanger onthult zijn identificatie pas als er een actief protocol wordt gestart. Een passieve waarnemer verneemt niets;
SIGMA-R β beschermt de ontvanger tegen actieve aanvallen, de initiator tegen passieve. Dit is precies omgekeerd, maar in dit protocol wordt er al vier handdrukberichten verzonden.We choose SIGMA-I as it resembles what we expect from client-server customary systems more closely: the client only recognizes the authenticated server, while the server knows everything. Additionally, it is simpler to implement due to fewer handshake messages. All we add to the protocol is the encryption of part of the message and the transfer of identifier A into the encrypted part of the last message:
βββββββ βββββββ βPeerAβ βPeerBβ ββββ¬βββ ββββ¬βββ β PubA, CookieA β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΆβ βSignPrvA, 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, ...)β β β βββββββββββββββββββββββ β β
- De signature gebruikt GOST R algoritme met 256-bits sleutels.
- Voor het genereren van een gemeenschappelijke sleutel wordt 34.10-2012 VKO gebruikt.
- CMAC wordt als MAC gebruikt. Technisch gezien is dit een speciale werkmodus van een blokversleuteling, beschreven in GOST R 34.13-2015. Voor deze modus wordt als versleutelingsfunctie β (34.12-2015).
- Als identificatie van de gesprekspartner wordt een hash van zijn openbare sleutel gebruikt. Voor de hash wordt (34.11-2012 256 bits) gebruikt.
Na de handdruk hebben we een gezamenlijke sleutel afgesproken. Deze kunnen we gebruiken voor geauthenticeerde versleuteling van transportberichten. Dit onderdeel is heel eenvoudig en moeilijk om verkeerd te doen: we verhogen de berichtenteller, versleutelen het bericht, authenticeren (MAC) de teller en de ciphertext, en sturen deze. Bij ontvangst van het bericht controleren we of de teller de verwachte waarde heeft, authenticeren de ciphertext met de teller, en decoderen deze. Met welke sleutel versleutelen we de handdrukberichten, het transport, en welke gebruiken we voor authenticatie? Het is gevaarlijk en onwijs om één sleutel voor al deze taken te gebruiken. Het is noodzakelijk om sleutels te genereren met behulp van gespecialiseerde functies. (key derivation function). Laten we niet te ingewikkeld doen en iets uitvinden: is al lang bekend, goed bestudeerd en heeft geen bekende problemen. Helaas is deze functie niet opgenomen in de standaardbibliotheek van Python, dus gebruiken we de module. HKDF gebruikt intern , die op zijn beurt de hashfunctie gebruikt. Een implementatie in Python die op de Wikipedia-pagina staat, vereist slechts een paar regels code. Zoals in het geval van 34.10-2012, zullen we Stribog-256 als hashfunctie gebruiken. De output van onze sleutelafstemmingsfunctie zal de sessiesleutel worden genoemd, waaruit de ontbrekende symmetrische sleutels zullen worden afgeleid:
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")Structuren/schema's
Laten we eens kijken naar welke ASN.1 structuren we nu hebben gekregen voor het verzenden van al deze gegevens:
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 β hetgeen dat getekend zal worden (te ondertekenen). HandshakeTBE β hetgeen dat versleuteld zal worden (te versleutelen). Let op het ukm-veld in MsgHandshake1. 34.10 VKO, voor een nog grotere randomisatie van de gegenereerde sleutels, bevat de parameter UKM (user keying material) β gewoon extra entropie.
Cryptografie toevoegen aan de code
Laten we alleen de wijzigingen in de oorspronkelijke code beschouwen, aangezien het raamwerk hetzelfde is gebleven (eigenlijk werd eerst de definitieve implementatie geschreven, en daarna werd alle cryptografie eruit gehaald).
Aangezien de authenticatie en identificatie van gesprekspartners op basis van publieke sleutels zal plaatsvinden, moeten ze nu ergens langdurig worden opgeslagen. Voor de eenvoud gebruiken we een JSON van dit type:
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }our β ons sleutel paar, hexadecimale privΓ©- en publieke sleutels. their β de namen van de gesprekspartners en hun publieke sleutels. Laten we de argumenten van de opdrachtregel wijzigen en de postprocessing van de JSON-gegevens toevoegen:
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="Genereer JSON met onze nieuwe keypair", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON met onze en hun sleutels", ) parser.add_argument( "--bind", default="::1", help="Adres om naar te luisteren", ) parser.add_argument( "--port", type=int, default=6666, help="Poort om naar te luisteren", ) 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 en unmarshallen van onze en hun sleutels {{{ 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), } # }}}De privΓ©sleutel van algoritme 34.10 is een willekeurig getal. Met een grootte van 256-bits voor 256-bits elliptische krommen. PyGOST werkt niet met een byte-array, maar met , daarom moet onze privΓ©sleutel (urandom(32)) worden omgevormd tot een getal met behulp van gost3410.prv_unmarshal(). De publieke sleutel wordt deterministisch berekend uit de privΓ©sleutel met behulp van gost3410.public_key(). De publieke sleutel van 34.10 bestaat uit twee grote getallen, die ook weer naar een byte-reeks moeten worden omgevormd voor gemakkelijke opslag en overdracht met behulp van gost3410.pub_marshal().
Na het lezen van het JSON-bestand moeten de publieke sleutels respectievelijk weer worden omgevormd met behulp van gost3410.pub_unmarshal(). Aangezien er identificatoren van gesprekspartners in de vorm van een hash van de publieke sleutel binnenkomen, kunnen deze meteen vooraf worden berekend en in een woordenboek worden geplaatst voor snelle toegang. De Stribog-256 hash is gost34112012256.GOST34112012256(), dat volledig voldoet aan de hashlib-interface van hash-functies.
Hoe is de coroutine van de initiator veranderd? Alles volgens het handdrukschema: we genereren een cookie (128-bits is ruim voldoende), een ephemeral keypair van 34.10, die zal worden gebruikt voor de VKO-functie van key agreement.
395 async def initiator(host, port): 396 _id = repr((host, port)) 397 logging.info("%s: aan het kiezen", _id) 398 reader, writer = await asyncio.open_connection(host, port) 399 # Genereer onze ephemere openbare sleutel en cookie, stuur Handshake 0 bericht {{{ 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()- we wachten op een antwoord en decoderen het ontvangen Msg-bericht;
- we zorgen ervoor dat we handshake1 hebben ontvangen;
- we decoderen de ephemere openbare sleutel van de tegenpartij en berekenen de sessiesleutel;
- we genereren de symmetrische sleutels die nodig zijn voor het verwerken van het TBE-gedeelte van het bericht.
423 logging.info("%s: kreeg %s bericht", _id, msg.choice) 424 if msg.choice != "handshake1": 425 logging.warning("%s: onverwacht bericht, verbindt niet meer", _id) 426 writer.close() 427 return 428 # }}} 429 msg_handshake1 = msg.value 430 # Valideer Handshake-bericht {{{ 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 is een 64-bits getal (urandom(8)), dat ook moet worden gedeserializeerd uit de byte-representatie met behulp van gost3410_vko.ukm_unmarshal(). De VKO-functie voor 34.10-2012 256-bits is gost3410_vko.kek_34102012256() (KEK β key encryption key).
De gegenereerde sessiesleutel is al een 256-bits byte pseudo-willekeurige reeks. Daarom kan deze onmiddellijk worden gebruikt in de HKDF-functie. Aangezien GOST34112012256 voldoet aan de hashlib-interface, kan deze onmiddellijk worden gebruikt in de Hkdf-klasse. We geven geen zout op (eerste argument Hkdf), omdat de gegenereerde sleutel door de ephemeral betrokken sleutelpaaren verschillend zal zijn voor elke sessie en deze al voldoende entropie heeft. kdf.expand() geeft standaard al sleutels van 256-bits lengte, die verder vereist zijn voor de Kikker.
Vervolgens worden de TBE- en TBS-gedeelten van het ontvangen bericht gecontroleerd:
- we berekenen en controleren een MAC over de ontvangen versleutelde tekst;
- we ontsleutelen de versleutelde tekst;
- we decoderen de TBE-structuur;
- hieruit halen we de identificatie van de gesprekspartner en controleren we of deze ΓΌberhaupt bekend is;
- we berekenen en controleren een MAC over deze identificatie;
- de handtekening boven de TBS-structuur wordt gecontroleerd, die zowel cookies van beide partijen als de publieke ephemeral key van de tegenovergestelde partij omvat. De handtekening wordt gecontroleerd met de langlevende handtekeningssleutel van de gesprekspartner.
441 probeer: 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 behalve ValueError als 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 als niet compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])): 140 raise ValueError("ongeldige MAC") 141 plaintext = ctr( 142 GOST3412Kuznechik(key_enc).encrypt, 143 KUZNECHIK_BLOCKSIZE, 144 ciphertext, 145 8 * b"x00", 146 ) 147 probeer: 148 tbe, _ = HandshakeTBE().decode(plaintext) 149 behalve ASN1Error: 150 raise ValueError("kan TBE niet decoderen") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 peer = KEYS.get(key_sign_pub_hash) 153 als peer is None: 154 raise ValueError("onbekende identiteit") 155 mac_tag = mac( 156 GOST3412Kuznechik(key_mac_identity).encrypt, 157 KUZNECHIK_BLOCKSIZE, 158 key_sign_pub_hash, 159 ) 160 als niet compare_digest(mac_tag, bytes(tbe["identityMac"])): 161 raise ValueError("ongeldige identiteit MAC") 162 tbs = HandshakeTBS(( 163 ("cookieTheir", cookie_their), 164 ("cookieOur", cookie_our), 165 ("pubKeyOur", pub_key_our), 166 )) 167 als niet gost3410.verify( 168 CURVE, 169 peer["pub"], 170 GOST34112012256(tbs.encode()).digest(), 171 bytes(tbe["signature"]), 172 ): 173 raise ValueError("ongeldige handtekening") 174 return peer["name"]Zoals eerder vermeld, beschrijft 34.13-2015 verschillende uit 34.12-2015. Onder hen is er een modus voor het genereren van een MAC-insertie. In PyGOST is dit gost3413.mac(). Deze modus vereist de overdracht van de versleutelingsfunctie (die één gegevensblok accepteert en retourneert), de grootte van het blok en de gegevens zelf. Waarom kan de grootte van het blok niet hardcoded worden? 34.12-2015 beschrijft niet alleen de 128-bits Kuznechik-versleuteling, maar ook de 64-bits β een licht gewijzigde variant van GOST 28147-89, die ooit door de KGB is ontwikkeld en nog steeds een van de hoogste beveiligingsniveaus heeft.
De ΠΆΠ΅Π½et wordt geΓ―nitialiseerd met gost.3412.GOST3412Kuznechik(key) en retourneert een object met .encrypt()/.decrypt() methoden, geschikt voor gebruik in 34.13 functies. MAC wordt als volgt berekend: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Voor het vergelijken van de berekende en binnengekomen MAC kan geen gewone vergelijking (==) van byte strings worden gebruikt, aangezien deze operatie tijdslekken veroorzaakt die in bepaalde gevallen kunnen leiden tot fatale kwetsbaarheden van het type aanvallen op TLS. In Python is er een speciale hmac.compare_digest functie voor dit doel.
De functie van de blokversleuteling kan slechts één blok gegevens versleutelen. Voor een groter aantal, en met een niet-multipel van de lengte, is het nodig om een versleutelingmodus te gebruiken. In 34.13-2015 worden de volgende beschreven: ECB, CTR, OFB, CBC, CFB. Elk heeft zijn eigen toelaatbare toepassingsgebieden en kenmerken. Tot onze grote spijt zijn er tot nu toe geen gestandaardiseerde (zoals CCM, OCB, GCM en vergelijkbare) β we zijn genoodzaakt om zelf ten minste een MAC toe te voegen. Ik kies voor (CTR): deze vereist geen aanvulling tot blokgrootte, kan worden parallel uitgevoerd, gebruikt alleen de versleutelingsfunctie en kan veilig worden gebruikt voor het versleutelen van een groot aantal berichten (in tegenstelling tot CBC, waar relatief snel botsingen beginnen te ontstaan).
Net als .mac(), accepteert .ctr() vergelijkbare gegevens als invoer: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Het is noodzakelijk om een initialisatievector op te geven, precies de helft van de grootte van de versleutelingsblokken. Als onze versleutelingssleutel uitsluitend wordt gebruikt voor het versleutelen van één bericht (zelfs als het uit meerdere blokken bestaat), kan een nul-initialisatievector veilig worden ingesteld. Voor het versleutelen van handshake-berichten wordt elke keer een aparte sleutel gebruikt.
De handtekeningcontrole gost3410.verify() is triviaal: we geven de elliptische curve op waarbinnen we werken (deze wordt gewoon vastgelegd in ons GOSTIM-protocol), de publieke sleutel van de ondertekenaar (vergeet niet dat dit een tuple van twee grote getallen moet zijn, niet een byte string), 34.11-2012 hash en de ontvangen handtekening.
Daarna bereiden we in de initiator het handshake2-bericht voor en sturen het, waarbij we dezelfde acties uitvoeren als bij de controle, alleen symmetrisch: ondertekenen met onze sleutels in plaats van controle, enz.
456 # Bereid en verzend Handshake 2 bericht {{{ 457 tbs = HandshakeTBS(( 458 ("cookieTheir", cookie_their), 459 ("cookieOur", cookie_our), 460 ("pubKeyOur", pub_our_raw), 461 )) 462 handtekening = 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 ("handtekening", OctetString(handtekening)), 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: sessie tot stand gebracht: %s", _id, peer_name)Wanneer de sessie is opgezet, worden de transport sleutels gegenereerd (een aparte sleutel voor encryptie, voor authenticatie, voor elke kant), en wordt Kuznechik geΓ―nitialiseerd voor decryptie en MAC-controle:
499 # Voer de tekstberichtzender uit, initialiseert 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 # Wacht op testberichten {{{ 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 coroutine versleutelt nu berichten voordat ze worden verzonden via de TCP-verbinding. Elk bericht heeft een monotonisch toenemende nonce, die ook fungeert als initialisatievector bij encryptie in de telmodus. Elk bericht en elk blok van het bericht zal gegarandeerd verschillende tellers bevatten.
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 += 1Binnenkomende berichten worden behandeld door de coroutine msg_receiver, die zich bezighoudt met authenticatie en decryptie:
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("onverwachte nonce-waarde") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("ongeldige 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)Conclusie
GOSTIM is uitsluitend bedoeld voor educatieve doeleinden (aangezien het niet is getest, op zijn minst)! De broncode van het programma kan worden gedownload (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Zoals al mijn projecten, type , , , , GOSTIM is volledig , verspreid onder de voorwaarden van .
, , lid van , Python/Go-ontwikkelaar, hoofd specialist .
Bron: habr.com
