GOSTIM: P2P F2F E2EE IM in één avond met GOST-encryptie

Als ontwikkelaar PyGOST 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 geen veilige protokol kunnen realiseren. 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

GOSTIM: P2P F2F E2EE IM in één avond met GOST-encryptie
peer-to-peer friend-to-friend, end-to-end versleuteld, instant messenger met SIGMA-I authenticatie- en sleutelovereenstemmingsprotocol (waarop is gebaseerd IPsec IKE ), waarbij uitsluitend GOST-cryptografische algoritmen van de PyGOST-bibliotheek en ASN.1 codering van berichten met dePyDERASN (waarover ik eerder al heb geschreven ). Een vereiste is dat het zo eenvoudig moet zijn dat je het in een avond (of werkdag) helemaal vanaf nul kunt schrijven, anders is het al geen eenvoudig programma meer. Het bevat ongetwijfeld fouten, overbodige complexiteit, tekortkomingen, plus dit is mijn eerste programma met de asyncio-bibliotheek.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. suckless projects, 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 tmux and/or multitail, one can achieve a multi-window interface with syntax highlighting. And with the help of rlwrap , 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 Go). 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 socat: 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 SCTP 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. XDR.

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 (waarover ik eerder al de bibliotheek, aangezien we zullen werken met hoge-level objecten die vaak prettiger en handiger zijn om mee te werken. In tegenstelling tot schemaloze bencode, MessagePack of CBOR, 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 perfect forward secrecy 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 Noise framework te gebruiken bij het opbouwen van protocollen, maar laten we iets eenvoudigers kiezen.

De meest populaire zijn twee protocollen:

  • TLS β€” 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.
  • IPsec met IKE β€” hebben geen serieuze cryptografische problemen, hoewel ze ook niet eenvoudig zijn. Als je over IKEv1 en IKEv2 leest, zijn hun oorsprongen STS, 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 sleutelovereenkomst: 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 reflection aanvallen, 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 34.10-2012 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 β€” Grashopper (34.12-2015).
    • Als identificatie van de gesprekspartner wordt een hash van zijn openbare sleutel gebruikt. Voor de hash wordt Stribog-256 (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. KDF (key derivation function). Laten we niet te ingewikkeld doen en iets uitvinden: HKDF 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 hkdf module. HKDF gebruikt intern HMAC, 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 grote getallen, 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 werkingsmodi van blokversleutelaars 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 Magma β€” 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 BEAST 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 geauthenticeerde versleutelingsmodi (zoals CCM, OCB, GCM en vergelijkbare) β€” we zijn genoodzaakt om zelf ten minste een MAC toe te voegen. Ik kies voor de tellermodus (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 += 1
    

    Binnenkomende 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 here (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Zoals al mijn projecten, type GoGOST, (waarover ik eerder al, NNCP, GoVPN, GOSTIM is volledig vrije software, verspreid onder de voorwaarden van GPLv3+.

    Sergej Matvejev, cryptopunk, lid van de SPO-stichting, Python/Go-ontwikkelaar, hoofd specialist FGUP "NTC 'Atlas'".

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers πŸ”₯ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster