0%

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 niet verifieert, praat met iedereen, en die helft sloot je met 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 , dus state["https"] is False en geen enkel antwoord heeft HSTS gedragen. Dit project zet op de server.

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 houdt je server begrensd. Standaard draait de handshake op het moment dat een verbinding geaccepteerd wordt, binnen 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

  1. Voeg in make_server een keyword-only context=None toe. Als er een context gegeven is, omhul je server.socket zoals hierboven en sla je het resultaat weer op in server.socket.

  2. Voeg in main() --certfile en --keyfile toe zoals beschreven.

  3. Schrijf nog drie tests in tests/test_attacks.py:

    • test_attack_page_refuses_inline_script logt in met steps.login_cookie(), vraagt om GET / met de , en assert dat de content-security-policy van het antwoord een default-src heeft en geen 'unsafe-inline' bevat.

    • test_attack_hsts_only_over_https assert dat GET /about geen strict-transport-security draagt zolang state["https"] False is, en hem wel draagt nadat de test state["https"] = True zet.

    • test_attack_client_context_verifies roept tls_contexts.client_context("certs/ca.pem") aan en assert dat verify_mode ssl.CERT_REQUIRED is en dat check_hostname True is. Het bestand importeert ssl en tls_contexts al.

  4. 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 die een handshake verwachtte.

  1. Voer python -m pytest -q uit. Vul daarna rij F13 van SECURITY.md in: noem de vier tests van dit hoofdstuk in de kolom Test en schrijf closed in 10.6 in 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 af te drukken. Het hulpprogramma verbergt ze. Op je eigen computer zie je ze, en de server blijft serveren.

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 waarvoor de adapter geen heeft, zoals 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 op de leeslijst, en sluit rij F13.

In reading_server.py:

  • make_server(handle, state, *, timeout=1.0, context=None): als context niet None is, zet je server.socket = context.wrap_socket(server.socket, server_side=True, do_handshake_on_connect=False). Zonder context is de server zoals voorheen.

  • main() neemt --certfile en --keyfile aan. Met beide roept hij tls_contexts.server_context(args.certfile, args.keyfile) aan, zet hij state["https"] = True en geeft hij context=... aan make_server. Met geen van beide blijft state["https"] False en wordt er geen context gemaakt. Met maar een van de twee roept hij parser.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 ingelogde draagt een content-security-policy met default-src en zonder 'unsafe-inline'.

  • test_attack_hsts_only_over_https: GET /about draagt geen strict-transport-security zolang state["https"] False is, en draagt hem wel als de vlag True is.

  • test_attack_client_context_verifies: tls_contexts.client_context("certs/ca.pem") heeft verify_mode gelijk aan ssl.CERT_REQUIRED en check_hostname gelijk aan True.

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 omhuld is. Hij draait 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.