0%

Vragen om van protocol te wisselen · oefening

De acceptwaarde berekenen

Je parser geeft de server nu een , en daarin bevat headers["sec-websocket-key"] de sleutel van de client als weggestripte . Voordat de server ja kan zeggen, heeft hij de ene nodig die zijn ja moet dragen: Sec-WebSocket-Accept, berekend uit die sleutel. Deze les berekent hem, en RFC 6455 geeft je een exact antwoord om tegen te controleren.

Wat de waarde bewijst

RFC 6455 sectie 1.3 legt uit waarom de server überhaupt iets moet berekenen. De server moet de client laten zien dat hij deze - heeft ontvangen en als zodanig heeft begrepen. Een server die geen WebSocket spreekt, of een cache tussen de twee die antwoordt met een opgeslagen antwoord, heeft geen reden om iets te berekenen uit een header die Sec-WebSocket-Key heet. Omdat de client voor elk verzoek een nieuwe sleutel kiest, past geen opgeslagen of verzonnen antwoord. Dus als de waarde in het antwoord degene is die de client verwacht, weet de client dat zijn verzoek iets heeft bereikt dat het begreep.

Meer bewijst het niet. Het recept hieronder is openbaar en gebruikt niets geheims, dus iedereen kan de waarde voor elke sleutel berekenen. Het laat niet zien wie de server draait, het versleutelt niets, en het is geen wachtwoord. Het is een controle dat beide kanten hetzelfde protocol spreken.

Het recept

De RFC legt nog één ingrediënt vast, een string van 36 tekens die elke WebSocket-server ter wereld gebruikt, geschreven in sectie 1.3 van de RFC:

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

Hij staat in ws_handshake.py als de constante GUID. De RFC koos hem omdat programma’s die WebSockets niet begrijpen hem waarschijnlijk niet gebruiken, en dat maakt het resultaat betekenisvol. De acceptwaarde is:

  1. de sleuteltekst, met GUID erachter geplakt;

  2. gecodeerd naar bytes met "ascii", omdat elk teken in beide ASCII is;

  3. gehasht met SHA-1, wat een digest van 20 bytes geeft;

  4. geschreven in , wat 28 tekens geeft;

  5. terug gedecodeerd naar een str, omdat hij als tekst in een header gaat.

Beide gereedschappen ken je uit with Python. hashlib maakte in hoofdstuk 8 een vingerafdruk van een body, daar met . SHA-1 werkt op dezelfde manier via hashlib.sha1: het neemt bytes aan en geeft een hash- terug waarvan .digest() de ruwe bytes is en .hexdigest() dezelfde waarde als hexadecimale tekst, twee tekens per byte. SHA-1 wordt niet meer vertrouwd voor beveiligingswerk, maar de RFC gebruikt het niet voor beveiliging; ze gebruikt het als een vast recept dat beide kanten kunnen volgen. base64 vervoerde in hoofdstuk 7 bytes als headertekst.

De digest en de hextekst zijn makkelijk te verwarren, dus vergelijk ze op een waarde die niets met WebSockets te maken heeft:

Try it

Het drukt 20 40 v5l2ih1SSK6sFm0W6rvnBjTvc4I= af. De 20 bytes van de digest worden 40 hexadecimale tekens, of 28 base64-tekens met één = als opvulling. De acceptwaarde is de base64 van de digest, de derde daarvan. De hextekst van 40 tekens is een andere waarde, en een client die hem vergelijkt met de 28 tekens die hij verwacht, weigert de handshake.

Wat drukt dit af?

import hashlib

sha = hashlib.sha1(b"x")
print(len(sha.digest()), len(sha.hexdigest()))

Tekst, geen gedecodeerde bytes

De sleutel is base64, en je decodeerde hem in les 1 om zijn 16 bytes te zien. Daardoor is het verleidelijk om de gedecodeerde bytes te hashen. De RFC doet dat niet: ze hasht de sleutel precies zoals de client hem stuurde, de 24 tekens van de headerwaarde zonder spaties eromheen, gevolgd door de GUID. Je parser heeft de waarde al gestript, dus key + GUID is zoals het er staat de juiste tekst.

Wat gaat er in SHA-1?

Voor de voorbeeldsleutel van de RFC, dGhlIHNhbXBsZSBub25jZQ==, is het resultaat de waarde die RFC 6455 afdrukt:

s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Als je iets anders geeft, vergelijk de stappen dan met de hierboven. Een waarde met de verkeerde lengte betekent meestal de hextekst in plaats van de digest, of een andere hash dan SHA-1. Een waarde met de juiste lengte die toch verschilt, betekent dat de invoer fout was: de GUID ontbreekt of is verkeerd getypt, of de sleutel is vóór het hashen gedecodeerd.

Je opdracht

Schrijf in ws_handshake.py accept_value(key): geef de base64-tekst van de SHA-1-digest van key + GUID, gecodeerd als ASCII, terug als str. Twee regels zijn genoeg. Laat de functies voor les 4 tot en met 6 zoals ze zijn.

Druk op Run. De sectie van les 3 in handshake_demo.py drukt nu de acceptwaarde voor de sleutel van de RFC af, onder de dictionary uit les 2:

Lesson 3: accept_value
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Dat is een regel uit een lokale run van de afgewerkte les. Vergelijk hem, teken voor teken, met de waarde van de RFC hierboven. Als ze overeenkomen, druk je op Submit. De controle roept accept_value aan met de sleutel van de RFC en met sleutels die je nog niet hebt gezien, en ze controleert dat het resultaat tekst is, geen bytes.

De server kan nu zijn bewijs berekenen. De volgende les beslist wanneer hij het stuurt, en wat hij in plaats daarvan stuurt als het verzoek er geen is dat hij moet accepteren.

Opdracht

Schrijf accept_value(key) in ws_handshake.py.

Plak de constante GUID achter de sleuteltekst, codeer het resultaat met "ascii", neem de SHA-1-digest met hashlib.sha1(...).digest(), codeer de digest met base64.b64encode, en geef hem terug als str. Gebruik de sleutel precies zoals je hem ontvangt, als tekst; decodeer hem niet eerst.

Laat de voor les 4 tot en met 6 zoals ze zijn. Druk op Run: de sectie van les 3 moet s3pPLMBiTxaQ9kYGzzhZRbK+xOo= afdrukken, de die RFC 6455 voor haar voorbeeldsleutel geeft.

Druk daarna op Submit. De controle roept accept_value aan met de sleutel van de RFC en met drie sleutels die je nog niet hebt gezien, vergelijkt elk resultaat met de waarde die het recept van de RFC geeft, en controleert dat het resultaat een str is. Als een resultaat fout is, noemt de melding de stap waar het op wijst: de GUID, de vorm van de digest, de hash, of de gedecodeerde sleutel. Het importeren van ws_handshake.py mag niets afdrukken.