0%

HTTPS, Zertifikate und Sicherheits-Header · Abschlussprojekt

Abschlussprojekt: Projekt: Die Leseliste über HTTPS

Zeile F13 von SECURITY.md ist die letzte offene Zeile, die dieser Kurs schließt. Sie hat zwei Hälften. Ein Client, der das nicht verifiziert, spricht mit jedem, und diese Hälfte hast du mit client_context() geschlossen. Die andere Hälfte ist die Seite: Keine Antwort hat einem Browser irgendetwas gesagt, und seit der letzten Lektion läuft jede Antwort, die dein Code erzeugt, durch harden(). Ein Zweig von harden() ist allerdings noch nie gelaufen. Dein Server spricht einfaches , also ist state["https"] False, und keine Antwort hat HSTS getragen. Dieses Projekt bringt auf den Server.

Den lauschenden Socket umhüllen

make_server baut einen HTTPServer, und dieser Server besitzt einen lauschenden , server.socket. Um TLS zu sprechen, umhüllst du diesen Socket mit einem Server-Kontext, bevor die erste Verbindung ankommt:

server.socket = context.wrap_socket(server.socket, server_side=True, do_handshake_on_connect=False)

Jede Verbindung, die der Server von da an annimmt, ist eine TLS-Verbindung. server_side=True sagt, welche Hälfte des Handshakes dieses Ende spielt.

Das dritte hält deinen Server begrenzt. Standardmäßig läuft der Handshake in dem Moment, in dem eine Verbindung angenommen wird, innerhalb von accept, wo kein Timeout gilt. Ein Client, der sich verbindet und dann nichts sagt, würde den Server dort festhalten, und das Limit für Verbindungen würde nie erreicht. Mit do_handshake_on_connect=False passiert der Handshake stattdessen beim ersten Lesen, und http_adapter.py lässt jedes Lesen auf einer Verbindung höchstens zwei Sekunden warten. Das Limit gilt für jedes einzelne Lesen, nicht für den Handshake als Ganzes: Ein Client, der immer nur ein paar Bytes sendet, kann eine Verbindung länger halten.

Python 3.14 hat außerdem http.server.HTTPSServer, der den Socket für dich umhüllt und seinen eigenen Kontext erstellt. Hier umhüllst du selbst, damit die Einstellung oben deine Wahl ist.

Warum übergibt make_server do_handshake_on_connect=False?

Sag dem Dienst, dass er hinter HTTPS steht

harden() kann die Verbindung nicht sehen. Es wird über state["https"] informiert. Also muss die Stelle, die entscheidet, ob der Server TLS spricht, auch das Flag setzen, und diese Stelle ist main(). Gib ihr zwei Optionen, --certfile und --keyfile. Wenn beide angegeben sind, baut main() mit tls_contexts.server_context() einen Kontext, setzt state["https"] = True und übergibt den Kontext an make_server. Wenn keine angegeben ist, ändert sich nichts. Die eine ohne die andere ist ein Fehler im Befehl, und parser.error() meldet ihn.

Der Server spricht TLS, und state["https"] wurde auf False gelassen. Was fehlt in seinen Antworten?

Deine Aufgabe

  1. Füg in make_server einen Keyword-only- context=None hinzu. Wenn ein Kontext übergeben wird, umhülle server.socket wie oben gezeigt und speichere das Ergebnis zurück in server.socket.

  2. Füg in main() --certfile und --keyfile wie beschrieben hinzu.

  3. Schreib drei weitere Tests in tests/test_attacks.py:

    • test_attack_page_refuses_inline_script meldet sich mit steps.login_cookie() an, fragt mit dem GET / an und sichert zu, dass die content-security-policy der Antwort ein default-src hat und 'unsafe-inline' nicht enthält.

    • test_attack_hsts_only_over_https sichert zu, dass GET /about kein strict-transport-security trägt, solange state["https"] False ist, und es trägt, nachdem der Test state["https"] = True gesetzt hat.

    • test_attack_client_context_verifies ruft tls_contexts.client_context("certs/ca.pem") auf und sichert zu, dass verify_mode ssl.CERT_REQUIRED ist und check_hostname True. Die Datei importiert ssl und tls_contexts bereits.

  4. Führ den Dienst über TLS aus:

python pl_sec.py https

Das Hilfsprogramm baut einen State, setzt state["https"] selbst und bedient ihn über dein make_server mit einem Kontext aus deinem 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

Der erste Block ist dein ganzer Dienst, der innerhalb von TLS antwortet, mit all seinen Headern. Der zweite ist dein eigener Client, der deinen eigenen Server ablehnt, weil er nach einem Namen gefragt hat, den das Zertifikat nicht auflistet. Der dritte ist ein Client, der einfaches HTTP an einen gesendet hat, der einen Handshake erwartete.

  1. Führ python -m pytest -q aus. Dann füll Zeile F13 von SECURITY.md aus: Nenn die vier Tests dieses Kapitels in der Spalte Test und schreib closed in 10.6 in die Spalte Status.

Ein Client sendet ein einfaches GET /about HTTP/1.1 an den TLS-Port. Was bekommt er?

Was das nicht getan hat

Der Lauf zeigt deinen Server, wie er auf diesem Computer mit Testzertifikaten TLS spricht. Über das Deployment zeigt er nichts. Dieses Kapitel hat kein Zertifikat ausgestellt, erneuert oder zurückgezogen, und es hat den Dienst nicht hinter einem vorgeschalteten Server betrieben.

Zwei weitere Grenzen. Wenn ein Client das Zertifikat ablehnt oder einfaches HTTP spricht, meldet http.server den fehlgeschlagenen Handshake, indem es etwa dreißig Zeilen ausgibt. Das Hilfsprogramm blendet sie aus. Auf deinem eigenen Computer wirst du sie sehen, und der Server bedient weiter.

Und „jede Antwort läuft durch harden()“ stimmt für jede Antwort, die dein Code erzeugt. Manche Fehlerseiten schreibt http.server selbst, bevor dein Code die Anfrage sieht: seine 501 für eine , für die der Adapter keine hat, etwa PATCH, seine 400 für eine Anfragezeile, die es nicht lesen kann, und seine Antworten auf eine Anfragezeile oder einen Header, der zu lang ist. Das sind HTML-Seiten ohne einen dieser Header. Und die 411, die closing_error() erzeugt, trägt über TLS kein HSTS: Diese Funktion hat keinen State, den sie fragen könnte. Das ist ein Grund, warum ein echter Dienst solche Header in seinem vorgeschalteten Server oder seinem Framework setzt, wo keine Antwort an ihnen vorbeikommt.

Drück Check my work. In SECURITY.md sind jetzt F1 bis F13 geschlossen und F14 absichtlich offen. Im nächsten Kapitel sagt dir niemand, wo du suchen sollst.

Aufgabe

Bring auf die Leseliste und schließ Zeile F13.

In reading_server.py:

  • make_server(handle, state, *, timeout=1.0, context=None): Wenn context nicht None ist, setz server.socket = context.wrap_socket(server.socket, server_side=True, do_handshake_on_connect=False). Ohne Kontext ist der Server wie zuvor.

  • main() nimmt --certfile und --keyfile. Mit beiden ruft es tls_contexts.server_context(args.certfile, args.keyfile) auf, setzt state["https"] = True und übergibt context=... an make_server. Mit keiner von beiden bleibt state["https"] False, und es wird kein Kontext erstellt. Mit nur einer davon ruft es parser.error(...) auf.

  • Alles, was Lektion 5 verlangt hat, gilt weiterhin.

Schreib in tests/test_attacks.py, neben dem Test aus Lektion 5:

  • test_attack_page_refuses_inline_script: Die Seite einer angemeldeten trägt eine content-security-policy mit default-src und ohne 'unsafe-inline'.

  • test_attack_hsts_only_over_https: GET /about trägt kein strict-transport-security, solange state["https"] False ist, und trägt es, wenn das Flag True ist.

  • test_attack_client_context_verifies: tls_contexts.client_context("certs/ca.pem") hat verify_mode gleich ssl.CERT_REQUIRED und check_hostname gleich True.

Füll in SECURITY.md Zeile F13 aus: die Namen der Tests, die sie beweisen, und closed in 10.6.

Führ python pl_sec.py https und python -m pytest -q im aus.

Dann drück Check my work. Die Prüfung wiederholt die Prüfungen aus Lektion 5. Sie ruft make_server mit Platzhaltern für die Serverklasse und den Kontext auf, sodass nichts lauscht und kein Handshake stattfindet, und sieht sich an, wie der umhüllt wurde. Sie führt main() mit Platzhaltern für make_server, serve und server_context aus, mit und ohne die beiden Optionen. Sie prüft die Caching- von POST /tokens, GET /me und einem Lesezugriff mit Token. Sie führt deine vier Tests gegen einen korrekten Server und Kontext aus, wo sie bestehen müssen, und jeden gegen den Fehler, den er bemerken muss, wo er fehlschlagen muss. Sie liest Zeile F13. Keine Prüfung öffnet eine Verbindung. Nichts aus deinem Terminal wird bewertet.