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 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 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 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 Only base64 data is allowed.
Een sleutel moet ook lang genoeg zijn. Een sleutel die korter is dan de 32 bytes van een 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 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 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 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
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.
Schrijf in
tests/test_attacks.pytest_attack_missing_key_is_refused(): controleer eerst datsigning.load_keys({"READING_KEY": steps.TEST_KEY})["current"]"29c565a5"is, zodat de test gaat over een lader die een goede sleutel accepteert; zet daarnarefused = False, roepsigning.load_keys({})aan binnentry, zetrefused = Trueinexcept ValueError, en controleerrefused. Hij moet eerst falen.Schrijf in
signing.pykey_from(environ, name). Leesenviron.get(name, ""). Lege tekst geeft eenmet een melding die de noemt. Decodeer met base64.b64decode(text, validate=True); tekst die geenis, of een sleutel die korter is dan KEY_BYTES, geeft ook eenValueError. Zet devan de variabele nooit in een melding. Geef de sleutelbytes terug. Schrijf
load_keys(environ): haal de sleutel op metkey_from(environ, "READING_KEY")en geef{"current": key_id(key), "by_id": {key_id(key): key}}terug.In
maininreading_server.py: alsenvironNoneis, gebruik danos.environ. Roepsigning.load_keys(environ)aan. Druk bij eenValueError(except ValueError as error) één regel die begint metreading_server: not starting:af naar standaardfout (print(..., file=sys.stderr)) en geef 1 terug. Sla anders het resultaat op instate["keys"]voordat de server gemaakt wordt.Vul rij F7 van
SECURITY.md, noem je test, en zet de status opclosed in 5.4.
Voer python -m pytest -q uit in de
Klik daarna op Check my work. Die roept load_keys aan met eigen 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.