HTTPS, certificaten en beveiligingsheaders · eindproject
Eindproject: Project: de leeslijst via HTTPS
Rij F13 van SECURITY.md is de laatste open rij die deze cursus sluit. Hij heeft twee helften. Een client die het client_context(). De andere helft is de pagina: geen enkel antwoord vertelde een browser iets, en sinds de vorige les verlaat elk antwoord dat je code maakt hem via harden(). Eén tak van harden() heeft echter nog nooit gedraaid. Je server spreekt gewone state["https"] is False en geen enkel antwoord heeft HSTS gedragen. Dit project zet
Omhul de luisterende socket
make_server bouwt een HTTPServer, en die server bezit een luisterende server.socket. Om TLS te spreken, omhul je die socket met een servercontext voordat de eerste verbinding binnenkomt:
server.socket = context.wrap_socket(server.socket, server_side=True, do_handshake_on_connect=False)
Elke verbinding die de server vanaf dan accepteert, is een TLS-verbinding. server_side=True zegt welke helft van de handshake dit eind speelt.
Het derde accept, waar geen time-out geldt. Een client die verbinding maakt en dan niets zegt, zou de server daar vasthouden, en de limiet op verbindingen zou nooit bereikt worden. Met do_handshake_on_connect=False gebeurt de handshake in plaats daarvan bij de eerste leesactie, en http_adapter.py laat elke leesactie op een verbinding hooguit twee seconden wachten. De limiet geldt per leesactie, niet voor de handshake als geheel: een client die steeds een paar bytes stuurt, kan een verbinding langer vasthouden.
Python 3.14 heeft ook http.server.HTTPSServer, die de socket voor je omhult en zijn eigen context maakt. Je doet het omhullen hier zelf, zodat de instelling hierboven jouw keuze is.
Waarom geeft make_server do_handshake_on_connect=False mee?
Vertel de service dat hij achter HTTPS zit
harden() kan de verbinding niet zien. Hij hoort het via state["https"]. Dus de plek die beslist of de server TLS spreekt, moet ook de vlag zetten, en die plek is main(). Geef hem twee opties, --certfile en --keyfile. Als beide gegeven zijn, bouwt main() een context met tls_contexts.server_context(), zet state["https"] = True en geeft de context aan make_server. Als geen van beide gegeven is, verandert er niets. De een zonder de ander is een fout in het commando, en parser.error() meldt die.
De server spreekt TLS, en state["https"] bleef False. Wat ontbreekt er in zijn antwoorden?
Je opdracht
Voeg in
make_servereen keyword-onlycontext=Nonetoe. Als er een context gegeven is, omhul jeserver.socketzoals hierboven en sla je het resultaat weer op inserver.socket.Voeg in
main()--certfileen--keyfiletoe zoals beschreven.Schrijf nog drie tests in
tests/test_attacks.py:test_attack_page_refuses_inline_scriptlogt in metsteps.login_cookie(), vraagt omGET /met de, en assert dat de content-security-policyvan het antwoord eendefault-srcheeft en geen'unsafe-inline'bevat.test_attack_hsts_only_over_httpsassert datGET /aboutgeenstrict-transport-securitydraagt zolangstate["https"]Falseis, en hem wel draagt nadat de teststate["https"] = Truezet.test_attack_client_context_verifiesroepttls_contexts.client_context("certs/ca.pem")aan en assert datverify_modessl.CERT_REQUIREDis en datcheck_hostnameTrueis. Het bestand importeertsslentls_contextsal.
Draai de service via TLS:
python pl_sec.py https
Het hulpprogramma bouwt een state, zet state["https"] zelf, en serveert die via je make_server met een context uit je server_context():
pl_sec: https, your server on 127.0.0.1 (a port the system chose), speaking TLS with certs/reading.pem
pl_sec set state["https"] to True, as your main() must when it is given a certificate. The name reading.example exists only in the model.
--- GET /about over TLS, expecting the name reading.example ---
HTTP/1.1 200 OK
content-type: application/json
content-length: 41
etag: "a595067de2a7c59a4be181e441d808ef93b456aca1f6d76b8583cd58d30abe1b"
cache-control: max-age=60
vary: Origin
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000
--- the same client, expecting the name other.example ---
refused before any request was sent: name
--- a client that speaks plain HTTP to the same port ---
the server closed the connection without an HTTP answer
--- result ---
the service answered over TLS, and its response carries strict-transport-security
serve() returned after 3 connections; the server is closed
pl_sec: every thread has stopped
Het eerste blok is je hele service die binnen TLS antwoordt, met al zijn headers. Het tweede is je eigen client die je eigen server weigert, omdat hij om een naam vroeg die het certificaat niet noemt. Het derde is een client die gewone HTTP stuurde naar een
Voer
python -m pytest -quit. Vul daarna rij F13 vanSECURITY.mdin: noem de vier tests van dit hoofdstuk in de kolom Test en schrijfclosed in 10.6in de kolom Status.
Een client stuurt een gewone GET /about HTTP/1.1 naar de TLS-poort. Wat krijgt hij?
Wat dit niet deed
De run laat je server TLS spreken op deze computer met testcertificaten. Hij laat niets zien over deployment. Dit hoofdstuk heeft geen certificaat uitgegeven, vernieuwd of ingetrokken, en het heeft de service niet achter een frontserver gedraaid.
Nog twee grenzen. Als een client het certificaat weigert, of gewone HTTP spreekt, meldt http.server de mislukte handshake door ongeveer dertig regels
En “elk antwoord verlaat de server via harden()” geldt voor elk antwoord dat je code maakt. Sommige foutpagina’s schrijft http.server zelf, voordat je code het verzoek ziet: zijn 501 voor een PATCH, zijn 400 voor een verzoekregel die hij niet kan lezen, en zijn antwoorden op een verzoekregel of een header die te lang is. Dat zijn HTML-pagina’s zonder een van deze headers. En de 411 die closing_error() maakt, draagt via TLS geen HSTS: die functie heeft geen state om te raadplegen. Dat is een van de redenen waarom een echte service zulke headers in zijn frontserver of zijn framework zet, waar geen enkel antwoord ze kan omzeilen.
Klik op Check my work. SECURITY.md heeft nu F1 tot en met F13 gesloten, en F14 met opzet open. In het volgende hoofdstuk vertelt niemand je waar je moet zoeken.
Opdracht
Zet
In reading_server.py:
make_server(handle, state, *, timeout=1.0, context=None): alscontextnietNoneis, zet jeserver.socket = context.wrap_socket(server.socket, server_side=True, do_handshake_on_connect=False). Zonder context is de server zoals voorheen.main()neemt--certfileen--keyfileaan. Met beide roept hijtls_contexts.server_context(args.certfile, args.keyfile)aan, zet hijstate["https"] = Trueen geeft hijcontext=...aanmake_server. Met geen van beide blijftstate["https"]Falseen wordt er geen context gemaakt. Met maar een van de twee roept hijparser.error(...)aan.Alles wat les 5 vroeg, geldt nog steeds.
Schrijf in tests/test_attacks.py, naast de test uit les 5:
test_attack_page_refuses_inline_script: de pagina van een ingelogdedraagt een content-security-policymetdefault-srcen zonder'unsafe-inline'.test_attack_hsts_only_over_https:GET /aboutdraagt geenstrict-transport-securityzolangstate["https"]Falseis, en draagt hem wel als de vlagTrueis.test_attack_client_context_verifies:tls_contexts.client_context("certs/ca.pem")heeftverify_modegelijk aanssl.CERT_REQUIREDencheck_hostnamegelijk aanTrue.
Vul in SECURITY.md rij F13 in: de namen van de tests die hem bewijzen, en closed in 10.6.
Voer python pl_sec.py https en python -m pytest -q uit in de
Klik daarna op Check my work. Die herhaalt de controles van les 5. Hij roept make_server aan met vervangers voor de serverklasse en de context, zodat er niets luistert en er geen handshake gebeurt, en kijkt hoe de main() met vervangers voor make_server, serve en server_context, met en zonder de twee opties. Hij controleert de cachewaarden van POST /tokens, GET /me en het lezen met een token. Hij draait je vier tests tegen een correcte server en context, waar ze moeten slagen, en elk tegen de fout die hij moet opmerken, waar hij moet falen. Hij leest rij F13. Geen enkele controle opent een verbinding. Niets uit je terminal wordt beoordeeld.