GOSTIM: P2P F2F E2EE IM på én aften med GOST-kryptering
At være udvikler PyGOST biblioteker (GOST kryptografiske primitiver i ren Python), modtager jeg ofte spørgsmål om, hvordan man implementerer den enkleste sikre beskeder på knæet. Mange mennesker anser anvendt kryptografi for at være ret simpelt, og at kalde .encrypt() på en blokchiffer vil være nok til at sende det sikkert over en kommunikationskanal. Andre mener, at anvendt kryptografi er nogle fås skæbne, og det er acceptabelt, at rige virksomheder kan lide Telegram med olympiade-matematikere ikke kan gennemføre sikker protokol.
Alt dette fik mig til at skrive denne artikel for at vise, at implementering af kryptografiske protokoller og sikker IM ikke er så vanskelig en opgave. Det er dog ikke værd at opfinde dine egne godkendelses- og nøgleaftaleprotokoller.
Artiklen vil skrive peer-to-peer, ven-til-ven, ende-til-ende krypteret instant messenger med SIGMA-I autentificering og nøgleaftaleprotokol (på grundlag af hvilken den implementeres IPsec IKE), udelukkende ved hjælp af GOST kryptografiske algoritmer PyGOST-bibliotek og ASN.1-meddelelseskodningsbibliotek PyDERASN (som jeg allerede skrev før). En forudsætning: det skal være så enkelt, at det kan skrives fra bunden på én aften (eller hverdag), ellers er det ikke længere et simpelt program. Det har sandsynligvis fejl, unødvendige komplikationer, mangler, plus dette er mit første program, der bruger asyncio-biblioteket.
IM design
Først skal vi forstå, hvordan vores IM vil se ud. For nemheds skyld, lad det være et peer-to-peer-netværk uden nogen opdagelse af deltagere. Vi vil personligt angive hvilken adresse: port der skal oprettes forbindelse til for at kommunikere med samtalepartneren.
Jeg forstår, at på nuværende tidspunkt er antagelsen om, at direkte kommunikation er tilgængelig mellem to vilkårlige computere, en væsentlig begrænsning af anvendeligheden af IM i praksis. Men jo flere udviklere implementerer alle mulige NAT-traversal krykker, jo længere vil vi forblive på IPv4 Internettet, med en deprimerende sandsynlighed for kommunikation mellem vilkårlige computere. Hvor længe kan du tolerere manglen på IPv6 derhjemme og på arbejdet?
Vi vil have et ven-til-ven-netværk: alle mulige samtalepartnere skal kendes på forhånd. For det første forenkler dette alt i høj grad: vi præsenterede os selv, fandt eller fandt ikke navnet/nøglen, afbrød forbindelsen eller fortsatte med at arbejde, idet vi kendte samtalepartneren. For det andet er det generelt sikkert og eliminerer mange angreb.
IM-grænsefladen vil være tæt på klassiske løsninger sugeløse projekter, som jeg virkelig godt kan lide for deres minimalisme og Unix-way filosofi. IM-programmet opretter en mappe med tre Unix-domæne-sockets til hver samtalepartner:
in—beskeder sendt til samtalepartneren optages i den;
ud - beskeder modtaget fra samtalepartneren læses fra den;
tilstand - ved at læse fra den finder vi ud af, om samtalepartneren i øjeblikket er tilsluttet, forbindelsesadressen/porten.
Derudover oprettes en conn socket, ved at skrive værtsporten, som vi initierer en forbindelse til den eksterne samtalepartner.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Denne tilgang giver dig mulighed for at lave uafhængige implementeringer af IM-transport og brugergrænseflade, fordi der ikke er nogen ven, du kan ikke behage alle. Ved brug af tmux og / eller multitail, kan du få en multi-vindues grænseflade med syntaksfremhævning. Og med hjælpen rlwrap du kan få en GNU Readline-kompatibel beskedinputlinje.
Faktisk bruger sugeløse projekter FIFO-filer. Personligt kunne jeg ikke forstå, hvordan man arbejder med filer konkurrencedygtigt i asyncio uden en håndskrevet baggrund fra dedikerede tråde (jeg har brugt sproget til sådanne ting i lang tid Go). Derfor besluttede jeg at nøjes med Unix-domæne-sockets. Desværre gør dette det umuligt at udføre echo 2001:470:dead::babe 6666 > conn. Jeg løste dette problem ved hjælp af socat: echo 2001:470:dead::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Den originale usikre protokol
TCP bruges som transport: det garanterer levering og dets bestilling. UDP garanterer hverken (hvilket ville være nyttigt, når der bruges kryptografi), men support SCTP Python kommer ikke ud af boksen.
Desværre er der i TCP ikke noget begreb om en besked, kun en strøm af bytes. Derfor er det nødvendigt at komme med et format for beskeder, så de kan deles indbyrdes i denne tråd. Vi kan aftale at bruge line feed-tegnet. Det er fint til at begynde med, men når vi først begynder at kryptere vores beskeder, kan dette tegn forekomme hvor som helst i chifferteksten. I netværk er populære protokoller derfor dem, der først sender længden af beskeden i bytes. For eksempel har Python ud af boksen xdrlib, som giver dig mulighed for at arbejde med et lignende format XDR.
Vi vil ikke arbejde korrekt og effektivt med TCP-læsning – vi vil forenkle koden. Vi læser data fra stikkontakten i en endeløs løkke, indtil vi afkoder hele beskeden. JSON med XML kan også bruges som format til denne tilgang. Men når kryptografi tilføjes, skal dataene signeres og autentificeres - og dette vil kræve en byte-for-byte identisk repræsentation af objekter, hvilket JSON/XML ikke leverer (dumps-resultater kan variere).
XDR er velegnet til denne opgave, dog vælger jeg ASN.1 med DER-kodning og PyDERASN bibliotek, da vi vil have genstande på højt niveau ved hånden, som det ofte er mere behageligt og bekvemt at arbejde med. I modsætning til skemaløse bencode, MessagePack eller CBOR, vil ASN.1 automatisk kontrollere dataene mod et hårdkodet skema.
Den modtagne besked vil være Msg: enten en tekst MsgText (med ét tekstfelt indtil videre) eller en MsgHandshake handshake besked (som indeholder navnet på samtalepartneren). Nu ser det overkompliceret ud, men dette er et fundament for fremtiden.
Indstil dit eget navn (--vores-navn alice). Alle forventede samtalepartnere er opført adskilt af kommaer (-deres navne bob, eve). For hver af samtalepartnerne oprettes en mappe med Unix-sokler, samt en koroutine for hver ind-, ud-tilstand:
Når du læser fra en tilstandsstik, søger programmet efter samtalepartnerens adresse i PEER_ALIVE-ordbogen. Hvis der endnu ikke er forbindelse til samtalepartneren, skrives en tom linje.
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()
Når du skriver en adresse til en stikkontakt, startes funktionen "initiator" af forbindelsen:
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))
Lad os overveje initiativtageren. Først åbner den åbenbart en forbindelse til den angivne vært/port og sender en håndtrykmeddelelse med dens navn:
Derefter venter den på et svar fra den eksterne part. Forsøger at afkode det indgående svar ved hjælp af Msg ASN.1-skemaet. Vi antager, at hele beskeden vil blive sendt i ét TCP-segment, og vi vil modtage den atomært, når vi kalder .read(). Vi tjekker, at vi har modtaget håndtryksmeddelelsen.
Vi kontrollerer, at det modtagne navn på samtalepartneren er kendt af os. Hvis ikke, så bryder vi forbindelsen. Vi tjekker, om vi allerede har etableret en forbindelse med ham (samtaleren gav igen kommandoen om at oprette forbindelse til os) og lukker den. IN_QUEUES-køen indeholder Python-strenge med teksten i beskeden, men har en speciel værdi på None, der signalerer, at msg_sender-koroutinen stopper med at virke, så den glemmer sin writer, der er forbundet med den gamle TCP-forbindelse.
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 accepterer udgående beskeder (i kø fra en in-socket), serialiserer dem til en MsgText-meddelelse og sender dem over en TCP-forbindelse. Den kan gå i stykker når som helst - vi opsnapper tydeligt dette.
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))
I slutningen kommer initiativtageren ind i en uendelig sløjfe af læsemeddelelser fra stikkontakten. Kontrollerer, om disse beskeder er tekstbeskeder, og placerer dem i OUT_QUEUES-køen, hvorfra de vil blive sendt til udgangen på den tilsvarende samtalepartner. Hvorfor kan du ikke bare lave .read() og afkode beskeden? Fordi det er muligt, at flere beskeder fra brugeren vil blive samlet i operativsystemets buffer og sendt i ét TCP-segment. Vi kan afkode den første, og så kan en del af den efterfølgende forblive i bufferen. I tilfælde af en unormal situation lukker vi TCP-forbindelsen og stopper msg_sender-coroutinen (ved at sende None til OUT_QUEUES-køen).
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)
Lad os vende tilbage til hovedkoden. Efter at have oprettet alle koroutinerne på det tidspunkt, hvor programmet starter, starter vi TCP-serveren. For hver etableret forbindelse opretter den en svarkoroutine.
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()
responder ligner initiator og afspejler alle de samme handlinger, men den uendelige løkke af læsning af beskeder starter med det samme, for nemheds skyld. I øjeblikket sender håndtryksprotokollen én besked fra hver side, men i fremtiden vil der være to fra forbindelsesinitiatoren, hvorefter der kan sendes sms’er med det samme.
Det er tid til at sikre vores kommunikation. Hvad mener vi med sikkerhed, og hvad ønsker vi:
fortrolighed af transmitterede meddelelser;
ægthed og integritet af transmitterede meddelelser - deres ændringer skal detekteres;
beskyttelse mod replay-angreb - faktum om manglende eller gentagne beskeder skal opdages (og vi beslutter at afbryde forbindelsen);
identifikation og autentificering af samtalepartnere ved hjælp af forudindtastede offentlige nøgler - vi besluttede allerede tidligere, at vi lavede et ven-til-ven-netværk. Først efter autentificering vil vi forstå, hvem vi kommunikerer med;
tilgængelighed perfekt fremadrettet hemmeligholdelse egenskaber (PFS) - kompromittering af vores langlivede signeringsnøgle bør ikke føre til muligheden for at læse al tidligere korrespondance. Optagelse af opsnappet trafik bliver ubrugelig;
gyldighed/gyldighed af meddelelser (transport og håndtryk) kun inden for én TCP-session. Det burde ikke være muligt at indsætte korrekt signerede/autentificerede beskeder fra en anden session (selv med den samme samtalepartner).
en passiv observatør bør hverken se brugeridentifikatorer, transmitterede offentlige nøgler med lang levetid eller hashes fra dem. En vis anonymitet fra en passiv iagttager.
Overraskende nok ønsker næsten alle at have dette minimum i enhver håndtryksprotokol, og meget lidt af ovenstående opfyldes i sidste ende for "hjemmelavede" protokoller. Nu opfinder vi ikke noget nyt. Jeg vil klart anbefale at bruge Støjramme til at bygge protokoller, men lad os vælge noget enklere.
De to mest populære protokoller er:
TLS - en meget kompleks protokol med en lang historie med fejl, jambs, sårbarheder, dårlig tankegang, kompleksitet og mangler (dette har dog lidt at gøre med TLS 1.3). Men vi overvejer det ikke, fordi det er overkompliceret.
IPsec с IKE — ikke har alvorlige kryptografiske problemer, selvom de heller ikke er enkle. Hvis du læser om IKEv1 og IKEv2, så er deres kilde STS, ISO/IEC IS 9798-3 og SIGMA (SIGn-and-MAc) protokoller - enkle nok til at implementere på én aften.
Hvad er godt ved SIGMA, som det seneste led i udviklingen af STS/ISO-protokoller? Det opfylder alle vores krav (inklusive at "skjule" samtalepartner-id'er) og har ingen kendte kryptografiske problemer. Det er minimalistisk - fjernelse af mindst ét element fra protokolmeddelelsen vil føre til dets usikkerhed.
Lad os gå fra den enkleste hjemmedyrkede protokol til SIGMA. Den mest basale operation, vi er interesseret i, er nøgleaftale: En funktion, der udsender begge deltagere den samme værdi, som kan bruges som en symmetrisk nøgle. Uden at gå i detaljer: hver af parterne genererer et flygtigt (bruges kun inden for én session) nøglepar (offentlige og private nøgler), udveksler offentlige nøgler, kalder aftalefunktionen, til hvis input de sender deres private nøgle og den offentlige nøgle. samtalepartnerens nøgle.
Enhver kan hoppe i midten og erstatte offentlige nøgler med deres egne - der er ingen godkendelse af samtalepartnere i denne protokol. Lad os tilføje en signatur med nøgler med lang levetid.
En sådan signatur vil ikke fungere, da den ikke er bundet til en bestemt session. Sådanne beskeder er også "egnede" til sessioner med andre deltagere. Hele konteksten skal abonnere. Dette tvinger os til også at tilføje en anden besked fra A.
Derudover er det vigtigt at tilføje din egen identifikator under signaturen, da vi ellers kan erstatte IdXXX og gensignere beskeden med nøglen fra en anden kendt samtalepartner. At forhindre refleksionsangreb, er det nødvendigt, at elementerne under signaturen er på klart definerede steder i henhold til deres betydning: hvis A tegner (PubA, PubB), så skal B underskrive (PubB, PubA). Dette taler også om vigtigheden af at vælge struktur og format for serialiserede data. For eksempel sorteres sæt i ASN.1 DER-kodning: SET OF(PubA, PubB) vil være identisk med SET OF(PubB, PubA).
Vi har dog stadig ikke "bevist", at vi har genereret den samme delte nøgle til denne session. I princippet kan vi undvære dette trin - den allerførste transportforbindelse vil være ugyldig, men vi ønsker, at når håndtrykket er gennemført, vil vi være sikre på, at alt virkelig er aftalt. I øjeblikket har vi ISO/IEC IS 9798-3-protokollen ved hånden.
Vi kunne underskrive selve den genererede nøgle. Dette er farligt, da det er muligt, at der kan være lækager i den anvendte signaturalgoritme (omend bits-per-signatur, men stadig lækager). Det er muligt at signere en hash af afledningsnøglen, men at lække selv hashen af den afledte nøgle kan være værdifuldt i et brute-force angreb på afledningsfunktionen. SIGMA bruger en MAC-funktion, der godkender afsender-id'et.
Som en optimering vil nogle måske genbruge deres flygtige nøgler (hvilket selvfølgelig er uheldigt for PFS). For eksempel genererede vi et nøglepar, forsøgte at oprette forbindelse, men TCP var ikke tilgængelig eller blev afbrudt et sted midt i protokollen. Det er en skam at spilde spildte entropi- og processorressourcer på et nyt par. Derfor vil vi introducere den såkaldte cookie - en pseudo-tilfældig værdi, der vil beskytte mod mulige tilfældige genafspilningsangreb ved genbrug af flygtige offentlige nøgler. På grund af bindingen mellem cookien og den flygtige offentlige nøgle, kan den modsatte parts offentlige nøgle fjernes fra signaturen som unødvendig.
Endelig ønsker vi at opnå privatlivets fred for vores samtalepartnere fra en passiv observatør. For at gøre dette foreslår SIGMA først at udveksle flygtige nøgler og udvikle en fælles nøgle til at kryptere autentificering og identificere meddelelser. SIGMA beskriver to muligheder:
SIGMA-I - beskytter initiatoren mod aktive angreb, responderen mod passive: initiatoren autentificerer responderen, og hvis noget ikke stemmer overens, giver den ikke sin identifikation. Den tiltalte udleverer sin identifikation, hvis der startes en aktiv protokol med ham. Den passive iagttager lærer intet;
SIGMA-R - beskytter responderen mod aktive angreb, initiativtageren mod passive. Alt er præcis det modsatte, men i denne protokol er der allerede sendt fire håndtrykmeddelelser.
Vi vælger SIGMA-I, da det minder mere om, hvad vi forventer af klient-server-kendte ting: Klienten genkendes kun af den autentificerede server, og alle kender allerede serveren. Derudover er det nemmere at implementere på grund af færre håndtryksmeddelelser. Alt vi tilføjer til protokollen er at kryptere en del af beskeden og overføre identifikatoren A til den krypterede del af den sidste besked:
GOST R bruges til signatur 34.10-2012 algoritme med 256-bit nøgler.
For at generere den offentlige nøgle bruges 34.10/2012/XNUMX VKO.
CMAC bruges som MAC. Teknisk set er dette en speciel driftsform for en blokchiffer, beskrevet i GOST R 34.13-2015. Som en krypteringsfunktion for denne tilstand − græshoppe (34.12-2015).
Hashen af hans offentlige nøgle bruges som samtalepartnerens identifikator. Brugt som hash Stribog-256 (34.11/2012/256 XNUMX bit).
Efter håndtrykket aftaler vi en fælles nøgle. Vi kan bruge det til autentificeret kryptering af transportmeddelelser. Denne del er meget enkel og svær at lave en fejl: vi øger meddelelsestælleren, krypterer meddelelsen, godkender (MAC) tælleren og chiffertekst, sender. Når vi modtager en besked, tjekker vi, at tælleren har den forventede værdi, autentificerer chifferteksten med tælleren og dekrypterer den. Hvilken nøgle skal jeg bruge til at kryptere håndtryksmeddelelser, transportere meddelelser, og hvordan man godkender dem? Det er farligt og uklogt at bruge én nøgle til alle disse opgaver. Det er nødvendigt at generere nøgler ved hjælp af specialiserede funktioner KDF (nøgleafledningsfunktion). Igen, lad os ikke flække hår og opfinde noget: HKDF har længe været kendt, godt undersøgt og har ingen kendte problemer. Desværre har det oprindelige Python-bibliotek ikke denne funktion, så vi bruger hkdf plastikpose. HKDF internt bruger HMAC, som igen bruger en hash-funktion. Et eksempel på implementering i Python på Wikipedia-siden tager kun et par linjer kode. Som i tilfældet med 34.10/2012/256 vil vi bruge Stribog-XNUMX som hash-funktion. Outputtet af vores nøgleaftalefunktion vil blive kaldt sessionsnøglen, hvorfra de manglende symmetriske vil blive genereret:
HandshakeTBS er, hvad der vil blive underskrevet. HandshakeTBE - hvad vil blive krypteret. Jeg henleder din opmærksomhed på ukm-feltet i MsgHandshake1. 34.10 VKO, for endnu større randomisering af de genererede nøgler, inkluderer parameteren UKM (brugernøglemateriale) - blot yderligere entropi.
Tilføjelse af kryptografi til kode
Lad os kun overveje ændringerne i den originale kode, da rammen forblev den samme (faktisk blev den endelige implementering skrevet først, og derefter blev al kryptografi skåret ud af den).
Da autentificering og identifikation af samtalepartnere vil blive udført ved hjælp af offentlige nøgler, skal de nu opbevares et sted i lang tid. For nemheds skyld bruger vi JSON som dette:
vores - vores nøglepar, hexadecimale private og offentlige nøgler. deres — navne på samtalepartnere og deres offentlige nøgler. Lad os ændre kommandolinjeargumenterne og tilføje efterbehandling af JSON-data:
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),
}
# }}}
Den private nøgle til 34.10-algoritmen er et tilfældigt tal. 256-bit størrelse til 256-bit elliptiske kurver. PyGOST virker ikke med et sæt bytes, men med store tal, så vores private nøgle (urandom(32)) skal konverteres til et tal ved hjælp af gost3410.prv_unmarshal(). Den offentlige nøgle bestemmes deterministisk ud fra den private nøgle ved hjælp af gost3410.public_key(). Den offentlige nøgle 34.10 er to store tal, der også skal konverteres til en byte-sekvens for at lette lagring og transmission ved hjælp af gost3410.pub_marshal().
Efter at have læst JSON-filen, skal de offentlige nøgler følgelig konverteres tilbage ved hjælp af gost3410.pub_unmarshal(). Da vi vil modtage identifikatorerne for samtalepartnerne i form af en hash fra den offentlige nøgle, kan de umiddelbart beregnes på forhånd og placeres i en ordbog til hurtig søgning. Stribog-256 hash er gost34112012256.GOST34112012256(), som fuldt ud opfylder hashlib-grænsefladen for hash-funktioner.
Hvordan har initiativtagerens koroutine ændret sig? Alt er som i håndtryksskemaet: vi genererer en cookie (128-bit er rigeligt), et flygtigt nøglepar 34.10, som vil blive brugt til VKO-nøgleaftalefunktionen.
UKM er et 64-bit nummer (urandom(8)), som også kræver deserialisering fra sin byte-repræsentation ved hjælp af gost3410_vko.ukm_unmarshal(). VKO funktion for 34.10/2012/256 3410-bit er gost34102012256_vko.kek_XNUMX() (KEK - krypteringsnøgle).
Den genererede sessionsnøgle er allerede en 256-bit pseudo-tilfældig byte-sekvens. Derfor kan den straks bruges i HKDF-funktioner. Da GOST34112012256 opfylder hashlib-grænsefladen, kan den straks bruges i Hkdf-klassen. Vi specificerer ikke saltet (det første argument for Hkdf), da den genererede nøgle, på grund af flygtigheden af de deltagende nøglepar, vil være forskellig for hver session og allerede indeholder nok entropi. kdf.expand() producerer som standard allerede de 256-bit nøgler, der kræves til Grasshopper senere.
Dernæst kontrolleres TBE- og TBS-delene af den indgående meddelelse:
MAC'en over den indkommende chiffertekst beregnes og kontrolleres;
chifferteksten er dekrypteret;
TBE struktur er afkodet;
samtalepartnerens identifikator tages fra den, og det kontrolleres, om han overhovedet er kendt af os;
MAC over denne identifikator beregnes og kontrolleres;
signaturen over TBS-strukturen er verificeret, hvilket inkluderer begge parters cookie og den modsatte parts offentlige flygtige nøgle. Signaturen verificeres af samtalepartnerens langlivede signaturnøgle.
Som jeg skrev ovenfor, beskriver 34.13/2015/XNUMX forskellige blokchiffer-driftstilstande fra 34.12. Blandt dem er der en tilstand til generering af imiterede indsatser og MAC-beregninger. I PyGOST er dette gost2015.mac(). Denne tilstand kræver beståelse af krypteringsfunktionen (modtagelse og returnering af en blok data), størrelsen af krypteringsblokken og faktisk selve dataene. Hvorfor kan du ikke hardkode størrelsen på krypteringsblokken? 3413/34.12/2015 beskriver ikke kun 128-bit Grasshopper-chifferet, men også 64-bit Magma - en let modificeret GOST 28147-89, skabt tilbage i KGB og stadig har en af de højeste sikkerhedstærskler.
Kuznechik initialiseres ved at kalde gost.3412.GOST3412Kuznechik(nøgle) og returnerer et objekt med .encrypt()/.decrypt()-metoder, der er egnede til at overføre til 34.13-funktioner. MAC beregnes som følger: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). For at sammenligne den beregnede og modtagne MAC kan du ikke bruge den sædvanlige sammenligning (==) af byte strenge, da denne operation lækker sammenligningstid, hvilket i det generelle tilfælde kan føre til fatale sårbarheder som f.eks. BEAST angreb på TLS. Python har en speciel funktion, hmac.compare_digest, til dette.
Blokkrypteringsfunktionen kan kun kryptere én datablok. For et større antal, og endda ikke et multiplum af længden, er det nødvendigt at bruge krypteringstilstanden. 34.13-2015 beskriver følgende: ECB, CTR, OFB, CBC, CFB. Hver har sine egne acceptable anvendelsesområder og egenskaber. Desværre har vi stadig ikke standardiseret autentificerede krypteringstilstande (såsom CCM, OCB, GCM og lignende) - vi er tvunget til i det mindste selv at tilføje MAC. jeg vælger tællertilstand (CTR): det kræver ikke polstring til blokstørrelsen, kan paralleliseres, bruger kun krypteringsfunktionen, kan sikkert bruges til at kryptere et stort antal meddelelser (i modsætning til CBC, som har kollisioner relativt hurtigt).
Ligesom .mac(), tager .ctr() lignende input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, almindelig tekst, iv). Det er påkrævet at specificere en initialiseringsvektor, der er nøjagtigt halvdelen af længden af krypteringsblokken. Hvis vores krypteringsnøgle kun bruges til at kryptere én besked (omend fra flere blokke), så er det sikkert at indstille en nul initialiseringsvektor. For at kryptere håndtryksmeddelelser bruger vi en separat nøgle hver gang.
At verificere signaturen gost3410.verify() er trivielt: vi passerer den elliptiske kurve, som vi arbejder inden for (vi registrerer den blot i vores GOSTIM-protokol), underskriverens offentlige nøgle (glem ikke, at dette skal være en tuple af to store tal og ikke en bytestreng), 34.11/2012/XNUMX hash og selve signaturen.
Dernæst forbereder og sender vi i initiativtageren en håndtryk-besked til handshake2, og udfører de samme handlinger, som vi gjorde under verifikationen, kun symmetrisk: signering på vores nøgler i stedet for at tjekke, osv...
Når sessionen er etableret, genereres transportnøgler (en separat nøgle til kryptering, til autentificering, for hver af parterne), og Grasshopperen initialiseres til at dekryptere og kontrollere MAC'en:
msg_sender coroutine krypterer nu meddelelser, før de sendes på en TCP-forbindelse. Hver besked har en monotont stigende nonce, som også er initialiseringsvektoren, når den krypteres i tællertilstand. Hver besked og beskedblok er garanteret at have en anden tællerværdi.
GOSTIM er beregnet til udelukkende at blive brugt til uddannelsesformål (da det i det mindste ikke er omfattet af tests)! Kildekoden til programmet kan downloades her (Стрибог-256 хэш: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как и все мои проекты, типа GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM er fuldstændig gratis softwarefordelt på vilkårene GPLv3 +.