GOSTIM: P2P F2F E2EE IM op één avond met GOST-cryptografie

Ontwikkelaar zijn PyGOST bibliotheken (GOST cryptografische primitieven in pure Python), krijg ik vaak vragen over hoe ik de eenvoudigste veilige berichtenuitwisseling op de knie kan implementeren. Veel mensen beschouwen toegepaste cryptografie als vrij eenvoudig, en het aanroepen van .encrypt() op een blokcode zal voldoende zijn om het veilig via een communicatiekanaal te verzenden. Anderen geloven dat toegepaste cryptografie het lot van weinigen is, en dat het acceptabel is dat rijke bedrijven als Telegram met olympiade-wiskundigen kan niet implementeren beveiligd protocol.

Dit alles bracht mij ertoe dit artikel te schrijven om te laten zien dat het implementeren van cryptografische protocollen en veilige IM niet zo'n moeilijke taak is. Het is echter niet de moeite waard om uw eigen authenticatie- en sleutelovereenkomstprotocollen te bedenken.

GOSTIM: P2P F2F E2EE IM op één avond met GOST-cryptografie
Het artikel zal schrijven peer-to-peer, vriend-tot-vriend, end-to-end gecodeerd instant messenger met SIGMA-I authenticatie- en sleutelovereenkomstprotocol (op basis waarvan het wordt geïmplementeerd IPsec-IKE), waarbij uitsluitend gebruik wordt gemaakt van de cryptografische algoritmen van GOST, de PyGOST-bibliotheek en de ASN.1-berichtcoderingsbibliotheek PyDERASN (waarover ik al eerder schreef). Voorwaarde: het moet zo eenvoudig zijn dat het in één avond (of werkdag) helemaal opnieuw kan worden geschreven, anders is het geen eenvoudig programma meer. Het bevat waarschijnlijk fouten, onnodige complicaties en tekortkomingen, en bovendien is dit mijn eerste programma dat de asyncio-bibliotheek gebruikt.

IM-ontwerp

Eerst moeten we begrijpen hoe onze IM eruit zal zien. Laat het voor de eenvoud een peer-to-peer-netwerk zijn, zonder enige ontdekking van deelnemers. Wij zullen persoonlijk aangeven op welk adres: poort we verbinding moeten maken om met de gesprekspartner te communiceren.

Ik begrijp dat op dit moment de veronderstelling dat directe communicatie beschikbaar is tussen twee willekeurige computers een aanzienlijke beperking vormt voor de toepasbaarheid van IM in de praktijk. Maar hoe meer ontwikkelaars allerlei soorten NAT-traversal-krukken implementeren, hoe langer we op het IPv4-internet zullen blijven, met een deprimerende kans op communicatie tussen willekeurige computers. Hoe lang kun je het gebrek aan IPv6 thuis en op het werk tolereren?

We zullen een vriend-tot-vriend-netwerk hebben: alle mogelijke gesprekspartners moeten vooraf bekend zijn. Ten eerste vereenvoudigt dit alles enorm: we hebben onszelf voorgesteld, de naam/sleutel gevonden of niet gevonden, de verbinding verbroken of verder gewerkt, terwijl we de gesprekspartner kenden. Ten tweede is het over het algemeen veilig en elimineert het veel aanvallen.

De IM-interface zal dicht bij klassieke oplossingen liggen waardeloze projecten, wat ik erg leuk vind vanwege hun minimalisme en Unix-filosofie. Het IM-programma maakt voor elke gesprekspartner een directory aan met drie Unix-domeinsockets:

  • in: berichten die naar de gesprekspartner worden verzonden, worden erin opgenomen;
  • uit - berichten ontvangen van de gesprekspartner worden ervan gelezen;
  • staat - door ervan te lezen, komen we erachter of de gesprekspartner momenteel verbonden is, het verbindingsadres/poort.

Bovendien wordt er een conn-socket gemaakt door de hostpoort te schrijven waarin we een verbinding met de externe gesprekspartner initiΓ«ren.

|-- alice
|   |-- in
|   |-- out
|   `-- state
|-- bob
|   |-- in
|   |-- out
|   `-- state
`- conn

Met deze aanpak kunt u onafhankelijke implementaties van IM-transport en gebruikersinterface maken, omdat er geen vriend is en u niet iedereen tevreden kunt stellen. Gebruik makend van tmux en / of meerstaartig, kunt u een interface met meerdere vensters krijgen met syntaxisaccentuering. En met de hulp rlwrap u kunt een GNU Readline-compatibele berichtinvoerregel krijgen.

In feite gebruiken suckless-projecten FIFO-bestanden. Persoonlijk kon ik niet begrijpen hoe ik competitief in asyncio met bestanden moest werken zonder een handgeschreven achtergrond uit speciale threads (ik gebruik de taal al heel lang voor zulke dingen Go). Daarom besloot ik het te doen met Unix-domeinsockets. Helaas maakt dit het onmogelijk om echo 2001:470:dead::babe 6666 > conn. Ik heb dit probleem opgelost met behulp van sokat: echo 2001:470:dood::schat 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Het originele, onveilige protocol

TCP wordt gebruikt als transportmiddel: het garandeert de levering en de bestelling ervan. UDP garandeert geen van beide (wat handig zou zijn als cryptografie wordt gebruikt), maar ondersteuning SCTP Python komt niet uit de doos.

Helaas is er in TCP geen concept van een bericht, alleen een stroom bytes. Daarom is het noodzakelijk om een ​​formaat voor berichten te bedenken, zodat ze in deze thread onderling kunnen worden gedeeld. We kunnen afspreken om het line feed-teken te gebruiken. Om te beginnen is dat prima, maar zodra we onze berichten gaan versleutelen, kan dit teken overal in de cijfertekst voorkomen. In netwerken zijn daarom populaire protocollen de protocollen die eerst de lengte van het bericht in bytes verzenden. Python beschikt bijvoorbeeld standaard over xdrlib, waarmee je met een vergelijkbaar formaat kunt werken XDR.

We zullen niet correct en efficiΓ«nt werken met TCP-lezen - we zullen de code vereenvoudigen. We lezen gegevens uit de socket in een eindeloze lus totdat we het volledige bericht decoderen. Ook JSON met XML kan als format voor deze aanpak worden gebruikt. Maar wanneer cryptografie wordt toegevoegd, moeten de gegevens worden ondertekend en geauthenticeerd - en dit vereist een byte-voor-byte identieke representatie van objecten, wat JSON/XML niet biedt (dumpresultaten kunnen variΓ«ren).

XDR is geschikt voor deze taak, maar ik kies voor ASN.1 met DER-codering en PyDERASN bibliotheek, omdat we objecten van hoog niveau bij de hand hebben waarmee het vaak prettiger en gemakkelijker is om te werken. In tegenstelling tot schemaloos Bencode, Berichtenpakket of CBOR, zal ASN.1 de gegevens automatisch controleren aan de hand van een hardgecodeerd 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 is Msg: een tekstbericht (voorlopig met één tekstveld) of een MsgHandshake-handshakebericht (dat de naam van de gesprekspartner bevat). Nu lijkt het ingewikkeld, maar dit is een basis voor de toekomst.

     β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ β”‚MsgHandshake(IdA) β”‚ │───────── ────────>β”‚ β”‚ β”‚ β”‚MsgHandshake(IdB) β”‚ β”‚<─────────────────│ β”‚ β”‚ β”‚ Berichttekst() β”‚ │──── MsgText() β”‚ β”‚ β”‚

IM zonder cryptografie

Zoals ik al zei, zal de asyncio-bibliotheek worden gebruikt voor alle socketbewerkingen. Laten we aankondigen wat we bij de lancering verwachten:

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(","))

Stel je eigen naam in (--onze-naam Alice). Alle verwachte gesprekspartners worden vermeld, gescheiden door komma's (β€”hun naam bob,eve). Voor elk van de gesprekspartners wordt een map met Unix-sockets gemaakt, evenals een coroutine voor elke in, uit, staat:

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 afkomstig van de gebruiker uit de in-socket worden naar de IN_QUEUES-wachtrij verzonden:

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 afkomstig van gesprekspartners worden naar OUT_QUEUES-wachtrijen gestuurd, van waaruit gegevens naar 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 een statussocket zoekt het programma naar het adres van de gesprekspartner in het PEER_ALIVE-woordenboek. Als er nog geen verbinding is met de gesprekspartner, wordt een lege regel 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()

Wanneer een adres naar een conn-socket wordt geschreven, wordt de functie β€œinitiator” van 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 eens kijken naar de initiatiefnemer. Eerst opent het uiteraard een verbinding met de opgegeven host/poort en verzendt het een handshake-bericht met zijn naam:

 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()

Vervolgens wacht het op een antwoord van de externe partij. Probeert het binnenkomende antwoord te decoderen met behulp van het Msg ASN.1-schema. We gaan ervan uit dat het volledige bericht in één TCP-segment wordt verzonden en dat we het atomair zullen ontvangen bij het aanroepen van .read(). We controleren of we het handdrukbericht hebben ontvangen.

 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     # }}}

Wij controleren of de ontvangen naam van de gesprekspartner bij ons bekend is. Zo niet, dan verbreken we de verbinding. We controleren of we al een verbinding met hem tot stand hebben gebracht (de gesprekspartner gaf opnieuw het bevel om verbinding met ons te maken) en sluiten deze af. De wachtrij IN_QUEUES bevat Python-tekenreeksen met de tekst van het bericht, maar heeft een speciale waarde van Geen die de coroutine van msg_sender signaleert om te stoppen met werken, zodat deze de schrijver vergeet die is gekoppeld aan de oude TCP-verbinding.

 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 accepteert uitgaande berichten (in de wachtrij geplaatst vanuit een in-socket), serialiseert ze in een MsgText-bericht en verzendt ze via een TCP-verbinding. Het kan elk moment kapot gaan - we onderscheppen dit duidelijk.

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))

Aan het einde komt de initiator in een oneindige lus van leesberichten uit de socket. Controleert of deze berichten tekstberichten zijn en plaatst ze in de wachtrij OUT_QUEUES, van waaruit ze naar de out-socket van de corresponderende gesprekspartner worden verzonden. Waarom kun je niet gewoon .read() uitvoeren en het bericht decoderen? Omdat het mogelijk is dat meerdere berichten van de gebruiker worden samengevoegd in de buffer van het besturingssysteem en in één TCP-segment worden verzonden. We kunnen de eerste decoderen, en een deel van de volgende kan in de buffer achterblijven. In geval van een abnormale situatie sluiten we de TCP-verbinding en stoppen we de coroutine msg_sender (door Geen naar de wachtrij OUT_QUEUES te sturen).

 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)

Laten we terugkeren naar de hoofdcode. Nadat we alle coroutines hebben aangemaakt op het moment dat het programma start, starten we de TCP-server. Voor elke tot stand gebrachte verbinding wordt een respondercoroutine gecreΓ«erd.

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()

de responder is vergelijkbaar met de initiator en weerspiegelt allemaal dezelfde acties, maar de oneindige lus van het lezen van berichten begint onmiddellijk, voor de eenvoud. Momenteel verzendt het handshakeprotocol één bericht van elke kant, maar in de toekomst zullen dat er twee zijn van de verbindingsinitiator, waarna er direct sms-berichten kunnen worden verzonden.

  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 te beveiligen. Wat verstaan ​​wij onder veiligheid en wat willen wij:

  • vertrouwelijkheid van verzonden berichten;
  • authenticiteit en integriteit van verzonden berichten - hun wijzigingen moeten worden gedetecteerd;
  • bescherming tegen replay-aanvallen - het feit van ontbrekende of herhaalde berichten moet worden gedetecteerd (en we besluiten de verbinding te verbreken);
  • identificatie en authenticatie van gesprekspartners met behulp van vooraf ingevoerde openbare sleutels - we hebben al eerder besloten dat we een vriend-tot-vriend-netwerk aan het maken waren. Pas na authenticatie zullen we begrijpen met wie we communiceren;
  • Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ perfecte voorwaartse geheimhouding Properties (PFS) - het compromitteren van onze langlevende ondertekeningssleutel mag niet leiden tot de mogelijkheid om alle eerdere correspondentie te lezen. Het opnemen van onderschept verkeer wordt nutteloos;
  • geldigheid/geldigheid van berichten (transport en handshake) alleen binnen één TCP-sessie. Het invoegen van correct ondertekende/geverifieerde berichten uit een andere sessie (zelfs met dezelfde gesprekspartner) zou niet mogelijk moeten zijn;
  • een passieve waarnemer mag geen gebruikersidentificaties, overgedragen openbare sleutels met een lange levensduur of hashes daarvan zien. Een zekere anonimiteit van een passieve waarnemer.

Verrassend genoeg wil bijna iedereen dit minimum in elk handdrukprotocol hebben, en bij protocollen van eigen bodem wordt uiteindelijk maar heel weinig van het bovenstaande gehaald. Nu zullen we niets nieuws bedenken. Ik zou het zeker aanraden om te gebruiken Ruis raamwerk voor het bouwen van protocollen, maar laten we iets eenvoudiger kiezen.

De twee meest populaire protocollen zijn:

  • TLS - een zeer complex protocol met een lange geschiedenis van bugs, stijlen, kwetsbaarheden, slecht nadenken, complexiteit en tekortkomingen (dit heeft echter weinig te maken met TLS 1.3). Maar we denken er niet over na, omdat het te ingewikkeld is.
  • IPsec с IKE – hebben geen ernstige cryptografische problemen, hoewel ze ook niet eenvoudig zijn. Als je leest over IKEv1 en IKEv2, dan is hun bron STS, ISO/IEC IS 9798-3 en SIGMA (SIGn-en-MAc) protocollen - eenvoudig genoeg om in één avond te implementeren.

Wat is er goed aan SIGMA, als nieuwste schakel in de ontwikkeling van STS/ISO-protocollen? Het voldoet aan al onze vereisten (inclusief het β€œverbergen” van identificatiegegevens van gesprekspartners) en kent geen cryptografische problemen. Het is minimalistisch: het verwijderen van ten minste één element uit het protocolbericht zal leiden tot onveiligheid ervan.

Laten we van het eenvoudigste protocol van eigen bodem naar SIGMA gaan. De meest elementaire bewerking waarin we geïnteresseerd zijn, is sleutel overeenkomst: Een functie die beide deelnemers dezelfde waarde oplevert, die kan worden gebruikt als een symmetrische sleutel. Zonder in details te treden: elk van de partijen genereert een kortstondig (slechts gebruikt binnen één sessie) sleutelpaar (publieke en private sleutels), wisselt publieke sleutels uit, roept de overeenkomstfunctie aan, aan de input waarvan ze hun private sleutel en de publieke sleutel doorgeven. sleutel van de gesprekspartner.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ β”‚ IdA, PubA β”‚ ╔══════════ ══════════╗ │───────────────>β”‚ β•‘PrvA, PubA = DHgen()β•‘ β”‚ β”‚ β•šβ• ════════ ═══════════╝ β”‚ IdB, PubB β”‚ ╔════════════════════╗ β”‚ <───────── ──────│ β•‘PrvB, PubB = DHgen()β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ─ ───┐ ╔════ ═══╧════════════╗ β”‚ β•‘Sleutel = DH(PrvA, PubB)β•‘ <β”€β”€β”€β”˜ β•šβ•β•β•β•β•β•β•β•€β• ═══════ ════╝ β”‚ β”‚ β”‚ β”‚

Iedereen kan in het midden springen en openbare sleutels vervangen door zijn eigen sleutels - er is geen authenticatie van gesprekspartners in dit protocol. Laten we een handtekening toevoegen met sleutels met een lange levensduur.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ β”‚IdA, PubA, teken(S ignPrvA, (PubA)) β”‚ ╔═ │──────────── ────────── ───────────>β”‚ β•‘SignPrvA, SignPub A = belasting()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β• ═══════ ═════════════╝ β”‚IdB, PubB, teken (SignPrvB, (PubB)) β”‚ ╔══════════════ ═══════ ══════╗ β”‚<─────── ─────────── ───────────── ──│ β•‘SignPrvB, SignPubB = laden( )β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β• ════════ ══════════════ ══╝ ────┐ ╔═══════════════ ═════╗ β”‚ β”‚ β•‘verifiΓ«ren( SignPubB, ...)β•‘ β”‚ <β”€β”€β”€β”˜ β•‘Sleutel = DH(Pr vA, PubB) β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ═╝ β”‚ β”‚ β”‚

Een dergelijke handtekening zal niet werken, omdat deze niet aan een specifieke sessie gebonden is. Dergelijke berichten zijn ook β€˜geschikt’ voor sessies met andere deelnemers. De gehele context moet zich abonneren. Dit dwingt ons om ook nog een bericht van A.

Bovendien is het van cruciaal belang om uw eigen identificatienummer onder de handtekening toe te voegen, omdat we anders IdXXX kunnen vervangen en het bericht opnieuw kunnen ondertekenen met de sleutel van een andere bekende gesprekspartner. Voorkomen reflectie aanvallen, is het noodzakelijk dat de elementen onder de handtekening zich op duidelijk gedefinieerde plaatsen bevinden, afhankelijk van hun betekenis: als A tekent (PubA, PubB), dan moet B tekenen (PubB, PubA). Dit spreekt ook over het belang van het kiezen van de structuur en het formaat van geserialiseerde gegevens. Sets in ASN.1 DER-codering worden bijvoorbeeld gesorteerd: SET OF(PubA, PubB) zal identiek zijn aan SET OF(PubB, PubA).

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ β”‚ IdA, PubA β”‚ ╔══════════ ═════════════════╗ │───────────────────── ────────── ─────────────>β”‚ β•‘SignPrvA, SignPubA = laden()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β• ═══════ ═════════hals ════ ════════════╗ β”‚<───────────────────────── ────────── ─────────│ β•‘SignPrvB, SignPubB = laden()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β• ════════ ══════════╝ β”‚ teken(SignPrvA, (IdA, PubB, PubA)) β”‚ ╔═════════════════ ═ ═══╗ │─ ──────────────────────────────────────── ───>β”‚ β•‘verifiΓ«ren(SignPubB, ...) β•‘ β”‚ β”‚ β•‘sleutel = dh (prva, PUBB) β•‘ β”‚ β”‚ β”‚

We hebben echter nog steeds niet β€˜bewezen’ dat we voor deze sessie dezelfde gedeelde sleutel hebben gegenereerd. In principe kunnen we het zonder deze stap stellen: de allereerste transportverbinding is ongeldig, maar we willen dat we er zeker van zijn dat alles echt is afgesproken als de handdruk is voltooid. Op dit moment hebben wij het ISO/IEC IS 9798-3 protocol bij de hand.

We kunnen de gegenereerde sleutel zelf ondertekenen. Dit is gevaarlijk, omdat het mogelijk is dat er lekken zijn in het gebruikte handtekeningalgoritme (hoewel bits per handtekening, maar nog steeds lekken). Het is mogelijk om een ​​hash van de afleidingssleutel te ondertekenen, maar zelfs het lekken van de hash van de afgeleide sleutel kan waardevol zijn bij een brute-force aanval op de afleidingsfunctie. SIGMA gebruikt een MAC-functie die de afzender-ID verifieert.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ β”‚ IdA, PubA β”‚ ╔══════════ ═════════════════╗ │───────────────────── ────────── ──────────────────>β”‚ β•‘SignPrvA, SignPubA = belasting()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•š ═══════ ════════════════════╝ β”‚IdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) β”‚ ╔════ ═ ══ β”‚<───────────────── ────────── ─────────── ────────── ─│ β•‘SignPrvB, SignPubB = load()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β• ═════════════ ════════ ══╝ β”‚ β”‚ ╔════════════ ═════════╗ β”‚ teken(SignPrvA, (PubB, PubA)), MAC(IdA) β”‚ β•‘Sleutel = DH( PrvA, PubB) β•‘ │───────────────────── ── ──────────── ───────── ─────>β”‚ β•‘verifiΓ«ren(sleutel, IdB) β•‘ β”‚ β”‚ β•‘verifiΓ«ren(SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ═════ ═╝ β”‚ β”‚

Als optimalisatie willen sommigen misschien hun kortstondige sleutels hergebruiken (wat natuurlijk jammer is voor PFS). We hebben bijvoorbeeld een sleutelpaar gegenereerd, geprobeerd verbinding te maken, maar TCP was niet beschikbaar of werd ergens midden in het protocol onderbroken. Het is zonde om verspilde entropie- en processorbronnen te verspillen aan een nieuw paar. Daarom zullen we de zogenaamde cookie introduceren - een pseudo-willekeurige waarde die bescherming biedt tegen mogelijke willekeurige herhalingsaanvallen bij het hergebruiken van kortstondige openbare sleutels. Door de binding tussen de cookie en de kortstondige publieke sleutel kan de publieke sleutel van de tegenpartij als onnodig uit de handtekening worden verwijderd.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ β”‚ IdA, PubA, CookieA β”‚ ╔════════ ═══════════════════╗ │─────────────────── ────────── ──────────────────────────────────────── ─>β”‚ β•‘SignPrvA, SignPubA = laden( )β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ══╝ β”‚IdB, PubB, CookieB , sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) β”‚ ╔═══════════════════════════ β•— β”‚< ──────────────────────────────────────── ────────── ────────────────────│ β•‘SignPrvB, SignPubB = belasting()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β• ═════════════════════╝ β”‚ β”‚ ╔══════════════ ═══════╗ β”‚ teken( SIGNPRVA, (cookieb, cookiea, puba)), Mac (ida) β”‚ β•‘Key = dh (prva, pubb) β•‘ │───-- JeCacCoel ──────────────────────────────────────── ───────>β”‚ β•‘ verifiΓ«ren(sleutel, IdB) β•‘ β”‚ β”‚ β•‘verifiΓ«ren(SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β”‚ β”‚

Tenslotte willen wij de privacy van onze gesprekspartners verkrijgen van een passieve waarnemer. Om dit te doen, stelt SIGMA voor om eerst kortstondige sleutels uit te wisselen en een gemeenschappelijke sleutel te ontwikkelen waarop authenticatie- en identificatieberichten kunnen worden gecodeerd. SIGMA beschrijft twee opties:

  • SIGMA-I - beschermt de initiator tegen actieve aanvallen, de responder tegen passieve aanvallen: de initiator authenticeert de responder en als iets niet overeenkomt, geeft hij zijn identificatie niet door. De verdachte geeft zijn identificatie af als er een actief protocol bij hem wordt gestart. De passieve waarnemer leert niets;
    SIGMA-R - beschermt de responder tegen actieve aanvallen, de initiator tegen passieve aanvallen. Alles is precies het tegenovergestelde, maar in dit protocol worden al vier handshake-berichten verzonden.

    We kiezen voor SIGMA-I omdat het meer lijkt op wat we verwachten van client-server vertrouwde dingen: de client wordt alleen herkend door de geauthenticeerde server en iedereen kent de server al. Bovendien is het eenvoudiger te implementeren omdat er minder handshake-berichten zijn. Het enige dat we aan het protocol toevoegen, is een deel van het bericht versleutelen en de identificatie A overbrengen naar het versleutelde deel van het laatste bericht:

    PubA, CookieA β”‚ ╔══════════ ═════════════════╗ │────── ────────── ───── ────────── ──────────────────────── ─────────── ───── ──────>β”‚ β•‘SignPrvA , SignPubA = laden()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β• ═══════ ═════════ ════╝ β”‚ PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) β”‚ ╔═════ ═══════════════ ═══════╗ β”‚<─────────────── ────────── ───── ────────── β•‘SignP rvB, SignPubB = laden()β•‘ β”‚ β”‚ β•‘ PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β• ═══════ ════════════════╝ β”‚ β”‚ ╔════════ ═════════hals ══╗ β”‚ Enc((IdA, teken( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) β”‚ β•‘Sleutel = DH(PrvA, PubB) β•‘ │───────────────────── ────────────────── ────────── ( SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β• ═══════ ══════════╝ β”‚ β”‚
    
    • GOST R wordt gebruikt voor ondertekening 34.10-2012 algoritme met 256-bit sleutels.
    • Voor het genereren van de publieke sleutel wordt gebruik gemaakt van 34.10-2012 VKO.
    • CMAC wordt gebruikt als MAC. Technisch gezien is dit een speciale werkingsmodus van een blokcijfer, beschreven in GOST R 34.13-2015. Als coderingsfunctie voor deze modus βˆ’ sprinkhaan (34.12-2015).
    • De hash van zijn publieke sleutel wordt gebruikt als identificatiemiddel van de gesprekspartner. Gebruikt als hasj Stribog-256 (34.11-2012-256 XNUMX bits).

    Na de handdruk spreken we een gedeelde sleutel af. We kunnen het gebruiken voor geauthenticeerde encryptie van transportberichten. Dit deel is heel eenvoudig en het is moeilijk om een ​​fout te maken: we verhogen de berichtenteller, coderen het bericht, authenticeren (MAC) de teller en de cijfertekst, verzenden. Wanneer we een bericht ontvangen, controleren we of de teller de verwachte waarde heeft, authenticeren we de cijfertekst met de teller en decoderen we deze. Welke sleutel moet ik gebruiken om handshake-berichten te coderen, berichten te verzenden en hoe ik ze kan authenticeren? Het is gevaarlijk en onverstandig om voor al deze taken één sleutel te gebruiken. Het is noodzakelijk om sleutels te genereren met behulp van gespecialiseerde functies KDF (sleutelafleidingsfunctie). Nogmaals, laten we geen haren splitsen en iets verzinnen: HKDF is al lang bekend, goed onderzocht en kent geen bekende problemen. Helaas heeft de native Python-bibliotheek deze functie niet, dus gebruiken we deze hkdf plastieken zak. HKDF maakt intern gebruik HMAC, die op zijn beurt een hash-functie gebruikt. Een voorbeeldimplementatie in Python op de Wikipedia-pagina vergt slechts een paar regels code. Net als in het geval van 34.10-2012-256 zullen we Stribog-XNUMX gebruiken als hashfunctie. De uitvoer van onze sleutelovereenkomstfunctie wordt de sessiesleutel genoemd, waaruit de ontbrekende symmetrische sleutels worden gegenereerd:

    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 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 is wat zal worden ondertekend. HandshakeTBE - wat wordt gecodeerd. Ik vestig uw aandacht op het ukm-veld in MsgHandshake1. 34.10 VKO bevat, voor een nog grotere randomisatie van de gegenereerde sleutels, de UKM-parameter (user keying material) - alleen maar extra entropie.

    Cryptografie aan code toevoegen

    Laten we alleen de wijzigingen bekijken die in de originele code zijn aangebracht, aangezien het raamwerk hetzelfde bleef (in feite werd eerst de definitieve implementatie geschreven en daarna werd alle cryptografie eruit gesneden).

    Omdat de authenticatie en identificatie van gesprekspartners zal plaatsvinden met behulp van publieke sleutels, moeten deze nu ergens voor langere tijd worden opgeslagen. Voor de eenvoud gebruiken we JSON als volgt:

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

    our - ons sleutelpaar, hexadecimale privΓ©- en publieke sleutels. hun β€” namen van gesprekspartners en hun publieke sleutels. Laten we de opdrachtregelargumenten wijzigen en naverwerking van 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="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),
        }
    # }}}
    

    De privΓ©sleutel van het 34.10-algoritme is een willekeurig getal. 256-bits grootte voor 256-bits elliptische curven. PyGOST werkt niet met een set bytes, maar met grote getallen, dus onze privΓ©sleutel (urandom(32)) moet worden geconverteerd naar een getal met behulp van gost3410.prv_unmarshal(). De publieke sleutel wordt deterministisch bepaald op basis van de private sleutel met behulp van gost3410.public_key(). De publieke sleutel 34.10 bestaat uit twee grote getallen die ook moeten worden omgezet in een bytereeks voor gemakkelijke opslag en verzending met behulp van gost3410.pub_marshal().

    Na het lezen van het JSON-bestand moeten de openbare sleutels dienovereenkomstig worden teruggeconverteerd met behulp van gost3410.pub_unmarshal(). Omdat we de identificatiegegevens van de gesprekspartners ontvangen in de vorm van een hash van de openbare sleutel, kunnen ze onmiddellijk vooraf worden berekend en in een woordenboek worden geplaatst voor snel zoeken. Stribog-256 hash is gost34112012256.GOST34112012256(), die volledig voldoet aan de hashlib-interface van hash-functies.

    Hoe is de initiatiefnemer coroutine veranderd? Alles verloopt volgens het handshake-schema: we genereren een cookie (128-bit is voldoende), een kortstondig sleutelpaar 34.10, dat zal worden gebruikt voor de VKO-sleutelovereenkomstfunctie.

     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()
    

    • we wachten op een antwoord en decoderen het binnenkomende bericht;
    • zorg ervoor dat je handdruk1 krijgt;
    • de kortstondige publieke sleutel van de tegenpartij decoderen en de sessiesleutel berekenen;
    • We genereren symmetrische sleutels die nodig zijn voor het verwerken van het TBE-gedeelte van het bericht.

     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 is een 64-bits getal (urandom(8)), dat ook deserialisatie vereist van zijn byterepresentatie met behulp van gost3410_vko.ukm_unmarshal(). VKO-functie voor 34.10-2012-256 3410-bit is gost34102012256_vko.kek_XNUMX() (KEK - coderingssleutel).

    De gegenereerde sessiesleutel is al een pseudo-willekeurige bytereeks van 256 bits. Daarom kan het onmiddellijk worden gebruikt in HKDF-functies. Omdat GOST34112012256 voldoet aan de hashlib-interface, kan het onmiddellijk worden gebruikt in de Hkdf-klasse. We specificeren de salt (het eerste argument van Hkdf) niet, omdat de gegenereerde sleutel, vanwege de kortstondigheid van de deelnemende sleutelparen, voor elke sessie anders zal zijn en al voldoende entropie bevat. kdf.expand() produceert standaard al de 256-bit sleutels die later nodig zijn voor Grasshopper.

    Vervolgens worden de TBE- en TBS-delen van het binnenkomende bericht gecontroleerd:

    • de MAC over de binnenkomende cijfertekst wordt berekend en gecontroleerd;
    • de cijfertekst wordt gedecodeerd;
    • TBE-structuur wordt gedecodeerd;
    • daaruit wordt de identifier van de gesprekspartner gehaald en gecontroleerd of hij ΓΌberhaupt bij ons bekend is;
    • MAC via deze identificatie wordt berekend en gecontroleerd;
    • de handtekening over de TBS-structuur wordt geverifieerd, inclusief de cookie van beide partijen en de openbare kortstondige sleutel van de andere partij. De handtekening wordt geverifieerd door de langlevende handtekeningsleutel van de gesprekspartner.

     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"]
    

    Zoals ik hierboven schreef, beschrijft 34.13-2015-XNUMX verschillende blokcoderingsmodi vanaf 34.12-2015-3413. Onder hen is er een modus voor het genereren van imitatie-inserts en MAC-berekeningen. In PyGOST is dit gost34.12.mac(). Deze modus vereist het doorgeven van de coderingsfunctie (het ontvangen en retourneren van één gegevensblok), de grootte van het coderingsblok en, in feite, de gegevens zelf. Waarom kun je de grootte van het coderingsblok niet hardcoderen? 2015-128-64 beschrijft niet alleen het XNUMX-bit Grasshopper-cijfer, maar ook het XNUMX-bit Magma - een licht gewijzigde GOST 28147-89, gemaakt in de KGB en heeft nog steeds een van de hoogste veiligheidsdrempels.

    Kuznechik wordt geΓ―nitialiseerd door gost.3412.GOST3412Kuznechik(key) aan te roepen en retourneert een object met .encrypt()/.decrypt()-methoden die geschikt zijn om door te geven aan 34.13-functies. MAC wordt als volgt berekend: gost3413.mac(GOST3412Kuznechik(sleutel).encrypt, KUZNECHIK_BLOCKSIZE, cijfertekst). Om de berekende en ontvangen MAC te vergelijken, kunt u niet de gebruikelijke vergelijking (==) van bytestrings gebruiken, aangezien deze bewerking vergelijkingstijd lekt, wat in het algemeen kan leiden tot fatale kwetsbaarheden zoals BEAST aanvallen op TLS. Python heeft hiervoor een speciale functie, hmac.compare_digest.

    De blokcoderingsfunctie kan slechts één gegevensblok coderen. Voor een groter aantal, en zelfs geen veelvoud van de lengte, is het noodzakelijk om de coderingsmodus te gebruiken. 34.13-2015 beschrijft het volgende: ECB, CTR, OFB, CBC, CFB. Elk heeft zijn eigen aanvaardbare toepassingsgebieden en kenmerken. Helaas hebben we nog steeds geen standaardisatie geverifieerde encryptiemodi (zoals CCM, OCB, GCM en dergelijke) - we zijn genoodzaakt om in ieder geval zelf MAC toe te voegen. ik kies teller modus (CTR): het vereist geen opvulling tot de blokgrootte, kan worden geparallelliseerd, gebruikt alleen de coderingsfunctie en kan veilig worden gebruikt om grote aantallen berichten te coderen (in tegenstelling tot CBC, dat relatief snel botsingen veroorzaakt).

    Net als .mac() heeft .ctr() soortgelijke invoer nodig: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Het is vereist om een ​​initialisatievector te specificeren die precies de helft van de lengte van het coderingsblok bedraagt. Als onze encryptiesleutel slechts wordt gebruikt om één bericht te versleutelen (zij het uit meerdere blokken), dan is het veilig om een ​​nul-initialisatievector in te stellen. Om handshakeberichten te versleutelen gebruiken wij telkens een aparte sleutel.

    Het verifiΓ«ren van de handtekening gost3410.verify() is triviaal: we passeren de elliptische curve waarbinnen we werken (we leggen deze eenvoudigweg vast in ons GOSTIM-protocol), de publieke sleutel van de ondertekenaar (vergeet niet dat dit een tupel van twee moet zijn grote getallen, en geen bytereeks), 34.11-2012-XNUMX-hash en de handtekening zelf.

    Vervolgens bereiden we in de initiator een handshakebericht voor en sturen dit naar handshake2, waarbij we dezelfde acties uitvoeren als tijdens de verificatie, alleen symmetrisch: onze sleutels ondertekenen in plaats van controleren, enz...

     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)
     

    Wanneer de sessie tot stand is gebracht, worden transportsleutels gegenereerd (een afzonderlijke sleutel voor codering, voor authenticatie, voor elk van de partijen) en wordt de Grasshopper geΓ―nitialiseerd om de MAC te decoderen en te controleren:

     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     # }}}
    

    De msg_sender coroutine codeert nu berichten voordat ze via een TCP-verbinding worden verzonden. Elk bericht heeft een monotoon toenemende nonce, die ook de initialisatievector is wanneer deze in de tellermodus wordt gecodeerd. Elk bericht en berichtblok heeft gegarandeerd een andere tellerwaarde.

    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
    

    Inkomende berichten worden verwerkt door de msg_receiver coroutine, die de authenticatie en decodering afhandelt:

    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)
    

    Conclusie

    GOSTIM is uitsluitend bedoeld voor educatieve doeleinden (aangezien het tenminste niet onder tests valt)! De broncode van het programma kan worden gedownload hier (Π‘Ρ‚Ρ€ΠΈΠ±ΠΎΠ³-256 Ρ…ΡΡˆ: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как ΠΈ всС ΠΌΠΎΠΈ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹, Ρ‚ΠΈΠΏΠ° GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM is volledig gratis software, verspreid onder de voorwaarden GPLv3 +.

    Sergey Matveev, cypherpunk, lid Stichting SPO, Python/Go-ontwikkelaar, hoofdspecialist FSUE "STC "Atlas".

Bron: www.habr.com

Voeg een reactie