16
Tags:
php
sikkerhed
kryptering
ssl
tls
Skrevet af
Bruger #2695
@ 10.05.2012
Introduktion
I sidste artikel så vi på digitale certifikater, som gør udvekslingen af offentlige nøgler lidt sikrere.
Nu kan vi udveksle nøgler, generere og udveksle hemmelige sessions nøgler, kryptere, padde, signere og en hel masse andet, men det tager jo en evighed, hvis vi skal implementere alt det bare for at lave en simpel klient og server.
Krypteret internet kommunikation er så brugbart og så almindeligt, at nogen har tænkt over det hele for os og lavet Transport Layer Security (TLS), som er en efterfølger til Secure Sockets Layer (SSL).
TLS er en "wrapper" rundt om vores almindelige sockets, som er filedescriptors, som bruges til netværks kommunikation. Man kan forbinde en socket til en server og derefter læse fra den eller skrive til den. Med TLS bliver der bare udvekslet digitale certifikater som det første i forbindelsen, og så vil al data, som sendes frem og tilbage blive krypteret og signeret af afsenderen og dekrypteret og valideret af modtageren. Har man implementeret en client/server software pakke, er det altså ret nemt at lægge et TLS lag ovenpå uden at skulle ændre en hel masse.
Igen er der mange detaljer, som måske er relevante for nogen, men jeg holder mig til det vigtigste, så må I selv "lege" videre med de mere avancerede ting...eller efterspørge noget konkret til en senere artikel :-)
Echo server
Vores TLS projekt bliver at lave en echo server og klient. En echo server er ultra simpel, du forbinder til den og så skriver du til den. Alt, hvad den modtager, sender den tilbage til dig.
Men vores echo server kræver, at alle klienter har et gyldigt certifikat, som er udstedt af vores egen CA, og så identificerer den sig selv med et certifikat, som også er udstedt af vores CA. Vi starter ud med nye filer, så lad os lave en CA:
$ php create_keypair.php CAPassword CA
$ php certificate_signing_request.php CA CAPassword 'The Playground CA' DK Aalborg 'The Playground' Jylland robert@the-playground.dk > CA_csr.pem
$ php certificate_sign.php CA_csr.pem CA CAPassword 365 > CA_cert.pem
$ rm -f CA_csr.pem CA.pub
Jeg slettede den offentlige nøgle, for den ligger i certifikatet og kan også udledes af den private nøgle. Nu vi er i gang laver vi lige et certifikat til vores server:
$ php create_keypair.php ServerPassword server
$ php certificate_signing_request.php server ServerPassword echo.the-playground.dk DK Aalborg 'The Playground' Jylland robert@the-playground.dk > server_csr.pem
$ php certificate_sign.php server_csr.pem CA CAPassword 365 CA_cert.pem > server_cert.pem
$ cat server_cert.pem server > server.pem
$ rm server.pub server_csr.pem server_cert.pem server
Det næstsidste jeg gjorde var at konkatenere to filer, certifikatet og den private nøgle, til én fil. Vi kan i PHP ikke specificere filerne separat overfor TLS, så det her er altså tricket. Fordi denne fil nu indeholder både serverns certifikat og private nøgle, så behøver jeg faktisk ikke alle de andre filer, så dem sletter jeg til sidst.
Jeg laver også lige certifikater til et par klienter:
$ php create_keypair.php RobertPassword robert
$ php certificate_signing_request.php robert RobertPassword 'Robert Larsen' DK Aalborg 'The Playground' Jylland robert@the-playground.dk > robert_csr.pem
$ php certificate_sign.php robert_csr.pem CA CAPassword 365 CA_cert.pem > robert_cert.pem
$ cat robert_cert.pem robert > robert.pem
$ php create_keypair.php PoulPassword poul
$ php certificate_signing_request.php poul PoulPassword 'Poul Poulsen' DK Aalborg 'The Playground' Jylland poul@the-playground.dk > poul_csr.pem
$ php certificate_sign.php poul_csr.pem CA CAPassword 365 CA_cert.pem > poul_cert.pem
$ cat poul_cert.pem poul > poul.pem
$ rm -f *_csr.pem *.pub robert_cert.pem robert poul_cert.pem poul
Godt, nu har jeg certifikater til to klienter og igen har jeg slettet alle unødvendige filer. Når vi har fået vores server til at køre, så vil vi prøve at forbinde ved hjælp af 'openssl' kommandoen fra Linux kommandolinjen.
Koden til serveren er følgende:
- <?php
- //Filename: ssl_echo_server.php
- if (count($argv) < 5) {
- echo "Usage: php " . $argv[0] . " <CA certificate> <server cert and key> <password> <port>\n";
- } else {
- //Konfiguration til vores sockets
- $context = stream_context_create(array(
- 'ssl' => array(
- 'verify_peer' => true,//Klienter skal have et gyldigt certifikat
- 'cafile' => $argv[1],//Dette er den eneste CA vi accepterer
- 'local_cert' => $argv[2],//Dette er serverens certifikat og privte nøgle
- 'passphrase' => $argv[3],//Dette er password til serverens private nøgle
- 'capture_peer_cert' => true,//Gør klientens certifikat tilgængelig for vores kode
- 'allow_self_signed' => false //Vi accepterer ikke self signed certificates
- )
- ));
-
- //Opret en server socket, som klienter kan forbinde til. Indiker SSL/TLS context options.
- if ($server = stream_socket_server('tcp://0.0.0.0:' . $argv[4], $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context)) {
- //Uendelig løkke
- while (true) {
- //Vi nulstiller 'peer_certificate' i serverens context
- stream_context_set_option($server, 'ssl', 'peer_certificate', null);
-
- //Vent på at nogen forbinder
- if ($client = @stream_socket_accept($server)) {
- //Brug TLS på denne socket
- //Denne funktion vil fejle, hvis klienten ikke sender et brugbart certifikat,
- //men fejler ikke hvis der slet ikke sendes et certifikat
- if (@stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) {
- //Denne indeholder klientens certifikat, hvis han sendte et
- $opts = stream_context_get_options($context);
- if (isset($opts['ssl']['peer_certificate']) && isset($opts['ssl']['peer_certificate']) !== null) {
- //Hent certifikatet ud
- $client_cert = $opts['ssl']['peer_certificate'];
- //Læs indholdet
- $info = openssl_x509_parse($client_cert);
- //Vi udskriver værdien af 'common name' attributten i hans 'distinctive name'.
- echo "Got connection from '" . $info['subject']['CN'] . "'\n";
-
- //Her stopper TLS delen...resten kunne ligeså godt arbejde med almindelige sockets
- //Vi starter med at læse en linje af gangen
- $again = true;
- while ($again && $line = fgets($client)) {
- //Fjern afsluttende newlines
- $line = trim($line);
- //Hvis klienten ønsker at afslutte...
- if ($line === 'exit') {
- //...så afslutter vi
- $again = false;
- } else {
- //...ellers skriver vi hans besked tilbage til ham
- fwrite($client, "You said '" . $line . "'\n");
- }
- }
- echo "Client closed\n";
- }
- }
- //Luk hans forbindelse
- fclose($client);
- }
- }
- }
- }
- ?>
Når nogen forbinder, så tjekkes hans certifikats gyldighed, hvis han har sendt et med. Hvis han ikke har, så springer vi direkte ned og lukker forbindelsen.
Vi kan tjekke, at serveren virker, ved at bruge 'openssl' programmet:
Først forsøgte jeg at forbinde til min server uden at autentificere mig med et certifikat. Det lykkedes ikke.
Derefter forsøgte jeg mig med henholdsvis 'robert.pem' og derefter med 'poul.pem', og det lykkedes. Serveren kunne udtrække brugerens navn fra certifikatet, så vi behøver ikke at sende brugernavn og password til serveren, som heller ikke behøver, at slå brugeren op i en database. Vores CA har sagt god for dem, og det stoler serveren på.
Echo klient
Serveren virker, men vi vil ikke bruge 'openssl' kommandoen, vi vil kode vores egen klient.
Den skal selvfølgelig forbinde til serveren, og vi ved, at serveren skal hedde 'echo.the-playground.dk', så det lader vi TLS tjekke for os. Når vi har en forbindelse, vil vi læse fra tastaturet og sende til serveren. Derefter forventer vi én linje fra serveren, som vi læser og skriver ud til standard out. Simpelt:
- <?php
- if (count($argv) < 6) {
- echo "Usage: php " . $argv[0] . " <CA certificate> <client cert and key> <password> <host> <port>\n";
- } else {
- //Konfiguration til vores sockets
- $context = stream_context_create(array(
- 'ssl' => array(
- 'verify_peer' => true, //Serveren skal have et gyldigt certifikat
- 'CN_match' => 'echo.the-playground.dk', //Common Name skal være echo.the-playground.dk
- 'cafile' => $argv[1], //Den eneste CA vi accepterer
- 'local_cert' => $argv[2], //Klientens certifikat og private nøgle
- 'passphrase' => $argv[3], //Password til privat nøgle
- 'allow_self_signed' => false //Ingen self signed certificates
- )
- ));
-
- //Transport til forbindelsen
- $transport = 'tcp://' . $argv[4] . ':' . $argv[5];
- //Lav en socket
- if ($socket = stream_socket_client($transport, $errno, $errstr, ini_get('default_socket_timeout'), STREAM_CLIENT_CONNECT, $context)) {
- //Ikke en non-blocking socket
- if (stream_set_blocking($socket, true)) {
- //Vær en TLS klient
- if (@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
- //Nåede vi så langt er vi forbundet og serveren har det forventede navn
- //Læs fra standard in
- while ($line = fgets(STDIN)) {
- //...og skriv til vores socket
- fwrite($socket, trim($line) . "\n");
- //Læs en linje fra vores socket
- if ($line = fgets($socket)) {
- //...og skriv til standard out
- echo trim($line) . "\n";
- } else {
- //...eller afbryd hvis vi ikke kunne læse
- break;
- }
- }
- }
- }
- fclose($socket);
- }
- }
- ?>
Vi prøver det af:
$ php ssl_echo_client.php
Usage: php ssl_echo_client.php <CA certificate> <client cert and key> <password> <host> <port>
$ php ssl_echo_client.php CA_cert.pem robert.pem RobertPassword localhost 5555
Hello, World!
You said 'Hello, World!'
Farvel!
You said 'Farvel!'
exit
Det virker! Prøv at sniffe forbindelsen med
Wireshark. Den er som forventet ret svær at læse.
HTTPS
Da mange af os har med web udvikling at gøre vil jeg lige tage et hurtigt kig på, hvordan HTTPS (HTTP med SSL) virker. For at kunne det, skal vi først lige dække lidt af HTTP protokollen.
En web server leverer data på baggrund af en HTTP forespørgsel. Hvis HTTP klienten vil have 'index.html' filen fra 'www.udvikleren.dk', så forbinder den til 'www.udvikleren.dk' port 80 og sender følgende forespørgsel:
GET /index.html HTTP/1.1
...plus det løse.
Men nu er det sådan, at samme server godt kan hoste flere domæners hjemmesider, så hvad nu hvis mit domæne (the-playground.dk) blev hostet på samme maskine ? Var det så 'index.html' fra udvikleren.dk eller fra the-playground.dk vi ønskede ?
Her kommer 'Host' headeren ind i billedet. Sammen med forespørgslen sender klienten en række headere, som uddyber forespørgslen:
GET /index.html HTTP/1.1
Host: www.udvikleren.dk
Sådan. Nu ved serveren, at den skal sende indholdet af 'index.html' fra 'www.udvikleren.dk', og det har man kunnet siden HTTP version 1.1, hvor Host headeren blev tilføjet. Men hvordan hænger det så sammen med TLS ?
Hvis begge domæner bliver hostet på samme server, så skal vi jo også kunne få to forskellige certifikater, afhængig af, hvilket domæne, vi vil tilgå. Vil vi have 'index.html' fra 'www.udvikleren.dk' så skal vi også have 'udvikleren.dk's certifikat.
Men certifikatet sendes, inden web klienten kan indikere, hvilken host den vil have data fra. Det giver et problem. Vi skal sende certifikatet, inden vi ved, hvilket certifikat vi skal sende.
I lang tid har der ikke været anden løsning end at bruge enten forskellige servere eller forskellige porte til hvert domæne, så 'udvikleren.dk' og 'the-playground.dk' enten lå på to forskellige IP adresser eller to forskellige porte, og det er også den løsning, som virker i flest browsere i dag. Men der er forskellige andre løsninger til problemet.
Ligesom HTTP protokollen fik Host headeren har TLS fået noget tilsvarende, nemlig 'Server Name Indication' (SNI). Her fortæller klienten hvilket certifikat, den vil have. Det løser jo alle problemer, men ikke alle browsere understøtter SNI, så man risikerer, at skære nogen af sine brugere fra.
En anden løsning er wildcards. Når browseren skal afgøre, om et certifikat tilhører det domæne, som den forbandt til, så kigger den på 'Common Name' (CN) delen af distinguished name. Hvis CN er det samme som host navnet, så er det det rigtige certifikat. Men man kan bruge et wildcard, så hvis CN er '*.udvikleren.dk' så dækker det både 'www.udvikleren.dk' og 'php.udvikleren.dk'. Men man kan ikke bruge et wildcard til at dække to forskellige domæner.
En tredje løsning er "Subject Alternative Names" feltet, som kan tilføjes et certifikat. Så kan et certifikat dække flere domæner, men nu har jeg ikke noget med udvikleren.dk at gøre, og Kasper har intet med the-playground.dk at gøre, så vi vil nok ikke dele certifikat. Men det kan bruges af firmaer, som har flere domæner. F.eks. 'dell.dk', 'dell.com', 'dell.se', osv. Dette burde virke i alle browsere.
Vores echo server autentificerede brugere ved at tjekke deres certifikat. Det gør HTTPS servere typisk ikke, de bruger SSL/TLS til at identificere sig selv overfor brugerne, men de
kan godt bede om et certifikat fra brugeren. Så skal certifikatet installeres i browseren (det er så forskelligt fra browser til browser hvordan man gør dette), og sådan kunne man nok have lavet NemID, men hvis man f.eks. bruger sin web server til Subversion server, så kan det give god mening at autentificere adgangen med klient certifikater.
Afslutning
Det var alt for denne gang. Som vi kan se, er det ikke så svært at sikre sine servere med TLS/SSL. Der er igen meget, vi ikke har gået i dybden med, men PHPs API er heller ikke så komplet, som man kunne have ønsket, men man kan da det mest nødvendige.
Hvad mangler vi at afdække ?
Kom med ønsker (evt. på udviklerens ønskeliste), og læg meget gerne en kommentar.
Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.
Del også gerne artiklen med dine Facebook venner:
Kommentarer (0)
Du skal være
logget ind for at skrive en kommentar.