0%

Ondertekende tokens en geheime sleutels · oefening

Laad de sleutel of weiger te starten

Start je server zoals een beheerder dat zou doen, voor één verbinding en met één seconde geduld:

python reading_server.py --max-connections 1 --idle-timeout 1

Hij drukt af dat hij luistert op , op een die het systeem koos, wacht een seconde op een client die nooit komt, en drukt served its connections and stopped af. Niemand vroeg hem om een sleutel. Een server die tokens gaat ondertekenen, mag zo niet starten.

Een sleutel als tekst

Een omgeving bevat tekst, en een sleutel is 32 willekeurige bytes. De sleutel reist dus als , de standaardsoort met opvulling. Open README.md: daarin staat de testsleutel voor deze cursus en hoe je de server ermee start. Die sleutel is met opzet leesbaar. Hij decodeert naar de woorden test-only key, never use it: 001, zodat niemand hem voor een echte kan aanzien.

Gebruik om de tekst weer in bytes om te zetten base64.b64decode(text, validate=True), zoals basic_credentials dat bij een login doet. Het tweede doet er hier meer toe dan daar. Zonder dat argument laat b64decode elk teken vallen dat hij niet kent en decodeert hij wat er overblijft:

import base64

text = "change this text to the real signing key before the server goes live"
print(len(base64.b64decode(text)))

Dat drukt 42 af. Een notitie die iemand in een instellingenbestand achterliet, is 42 bytes sleutel geworden, en iedereen die het bestand gezien heeft, kent ze. Met validate=True geeft dezelfde aanroep een binascii.Error, een soort , met de melding Only base64 data is allowed.

Een sleutel moet ook lang genoeg zijn. Een sleutel die korter is dan de 32 bytes van een -digest, is het zwakke deel van de tag, en daarom maakte hoofdstuk 2 sleutels van 32 bytes. signing.py heeft dat getal als KEY_BYTES.

Dicht falen

Als niet aan een beveiligingseis is voldaan, weiger dan. Die regel heet dicht falen (fail closed), en de laatste vraag van les 3 ging erover. Elke andere keuze laat de dienst er gezond uitzien terwijl hij dat niet is: een ingebouwde sleutel die iedereen kent, een willekeurige sleutel die bij elke herstart elk token beëindigt, of een server die bereikbaar is en iedereen weigert.

Een ontwikkelaar schrijft environ.get("READING_KEY", "dev-key"). Wat kost die standaardwaarde?

Schrijf eerst de aanval

De aanval is een server die opstart zonder sleutel in zijn omgeving. Als test: signing.load_keys({}) moet een ValueError geven.

Een lader die voor alles een ValueError gaf, zou die test doorstaan en nutteloos zijn, dus test_attack_missing_key_is_refused begint met een vooraf: de testsleutel laadt. steps.TEST_KEY bevat dezelfde tekst als README.md, en signing.load_keys({"READING_KEY": steps.TEST_KEY})["current"] moet "29c565a5" zijn. Daarna komt de weigering. Deze cursus gebruikt pytest.raises niet: zet een vlag op False, roep de aan binnen try, zet de vlag op True in except ValueError, en controleer de vlag.

Voer python -m pytest -q uit. De nieuwe test faalt op zijn eerste regel, omdat load_keys nog NotImplementedError geeft:

1 failed, 13 passed

De mapping is een argument

load_keys(environ) leest alleen de die hij krijgt. Hij importeert os niet, en kijkt nooit naar de echte omgeving. main geeft os.environ mee, een test geeft {} mee, en beide draaien dezelfde code.

handle() kijkt helemaal nooit naar de omgeving. main slaat op wat load_keys teruggaf in state["keys"], en vanaf les 6 vindt de verzoekcode daar zijn sleutels.

Zeg waarom, op de juiste stream, zonder het geheim

Als load_keys een ValueError geeft, vangt main die op, drukt één regel af naar standaardfout, en geeft 1 terug. Vang hem op als except ValueError as error, en druk af met file=sys.stderr. main heeft hier al een environ voor; als die None is, gebruik dan os.environ.

Standaardfout en een exitcode die niet nul is, zijn hoe wat de server ook startte te weten komt dat hij niet gestart is. De regel noemt de en herhaalt nooit de ervan. Een foutmelding is ergens een logregel, en les 3 noemde een log bij de plekken waar een sleutel niet mag staan. Dat geldt ook voor een verkeerde waarde: een sleutel waar één teken aan ontbreekt, is nog steeds het grootste deel van een sleutel.

Waarom drukt main de weigering af naar standaardfout en geeft hij 1 terug, in plaats van af te drukken naar standaarduitvoer en 0 terug te geven?

Wat load_keys teruggeeft

Het werk is over twee functies verdeeld. key_from(environ, name) leest één variabele en geeft de sleutelbytes terug, of geeft een ValueError. load_keys(environ) roept hem aan voor READING_KEY en geeft een dict terug:

{"current": "29c565a5", "by_id": {"29c565a5": b"test-only key, never use it: 001"}}

Met één sleutel is dat meer vorm dan je nodig hebt. De volgende les voegt een tweede sleutel toe, en verder hoeft er niets te veranderen. De korte tekst is een sleutel-id, uit de meegeleverde key_id(key): de eerste 8 hexcijfers van de SHA-256-digest van de sleutel. Het is een openbare naam voor iets geheims. Een digest kan niet worden teruggezet in een sleutel van 32 willekeurige bytes.

Je opdracht

Schrijf de test en zie hem falen. Schrijf daarna key_from en load_keys in signing.py, en laad de sleutels in main in reading_server.py, bij het commentaar dat de plek aangeeft. De oefening noemt de stappen. Voer daarna beide commando’s uit:

python reading_server.py; echo "exit code: $?"
READING_KEY=dGVzdC1vbmx5IGtleSwgbmV2ZXIgdXNlIGl0OiAwMDE= python reading_server.py --max-connections 1 --idle-timeout 1; echo "exit code: $?"

Het eerste drukt één regel af en de exitcode. Als je key_from READING_KEY is not set zegt, luidt die:

reading_server: not starting: READING_KEY is not set
exit code: 1

Het tweede luistert, wacht één seconde op een client die nooit komt, stopt, en drukt exit code: 0 af. Voer python -m pytest -q opnieuw uit: 14 passed.

Vul als laatste rij F7 van SECURITY.md. Wat er blootgesteld is, is de macht om een token voor elke gebruiker te ondertekenen. Noem je test in de cel Test en zet de status op closed in 5.4.

Klik daarna op Check my work. Je server heeft nu een sleutel met een thuis. Hij heeft er wel precies één, en geen manier om hem te vervangen zonder in één keer elk token te beëindigen. Dat is de volgende les.

Opdracht

Laat de server zijn sleutel uit de omgeving laden, en zonder sleutel weigeren te starten.

  1. Schrijf in tests/test_attacks.py test_attack_missing_key_is_refused(): controleer eerst dat signing.load_keys({"READING_KEY": steps.TEST_KEY})["current"] "29c565a5" is, zodat de test gaat over een lader die een goede sleutel accepteert; zet daarna refused = False, roep signing.load_keys({}) aan binnen try, zet refused = True in except ValueError, en controleer refused. Hij moet eerst falen.

  2. Schrijf in signing.py key_from(environ, name). Lees environ.get(name, ""). Lege tekst geeft een met een melding die de noemt. Decodeer met base64.b64decode(text, validate=True); tekst die geen is, of een sleutel die korter is dan KEY_BYTES, geeft ook een ValueError. Zet de van de variabele nooit in een melding. Geef de sleutelbytes terug.

  3. Schrijf load_keys(environ): haal de sleutel op met key_from(environ, "READING_KEY") en geef {"current": key_id(key), "by_id": {key_id(key): key}} terug.

  4. In main in reading_server.py: als environ None is, gebruik dan os.environ. Roep signing.load_keys(environ) aan. Druk bij een ValueError (except ValueError as error) één regel die begint met reading_server: not starting: af naar standaardfout (print(..., file=sys.stderr)) en geef 1 terug. Sla anders het resultaat op in state["keys"] voordat de server gemaakt wordt.

  5. Vul rij F7 van SECURITY.md, noem je test, en zet de status op closed in 5.4.

Voer python -m pytest -q uit in de , en de twee commando’s uit de les om de server te zien weigeren en daarna starten.

Klik daarna op Check my work. Die roept load_keys aan met eigen : geen sleutel, een lege, sleutels die te kort zijn, tekst die geen base64 is, en goede sleutels die je niet hebt gezien. Een weigering moet een ValueError zijn waarvan de melding de waarde niet herhaalt. Hij roept key_from aan met een andere variabelenaam, en main via zijn environ, zonder dat er iets luistert: zonder sleutel mag main geen server bouwen, moet hij een code teruggeven die niet nul is, en precies één regel naar standaardfout schrijven. Hij voert je test drie keer uit, elke keer met andere sleutelfuncties op de plaats van die in signing.py: een correcte lader, waar je test moet slagen; een lader met een ingebouwde sleutel, waar hij moet falen; en een lader die elke sleutel weigert, waar hij ook moet falen. Hij leest ook rij F7 van SECURITY.md; de formulering van je bevinding wordt niet beoordeeld. Je echte omgeving wordt nooit gelezen. Niets uit je terminal wordt beoordeeld.