├── .gitignore ├── .vscode └── settings.json ├── tcp └── server.go ├── http └── server.go ├── https └── server.go ├── tls └── server.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "netcat", 4 | "stdlib", 5 | "Wireshark" 6 | ] 7 | } -------------------------------------------------------------------------------- /tcp/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | ) 8 | 9 | func main() { 10 | ln, err := net.Listen("tcp", "localhost:1234") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | defer ln.Close() 15 | 16 | for { 17 | conn, err := ln.Accept() 18 | if err != nil { 19 | log.Print(err) 20 | continue 21 | } 22 | go echo(conn) 23 | } 24 | } 25 | 26 | func echo(conn net.Conn) { 27 | io.Copy(conn, conn) 28 | conn.Close() 29 | } 30 | -------------------------------------------------------------------------------- /http/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | http.HandleFunc("/", echo) 11 | log.Fatal(http.ListenAndServe("localhost:8080", nil)) 12 | } 13 | 14 | func echo(resp http.ResponseWriter, req *http.Request) { 15 | reqBody, err := io.ReadAll(req.Body) 16 | if err != nil { 17 | http.Error(resp, 18 | "Error reading request body", 19 | http.StatusInternalServerError) 20 | return 21 | } 22 | defer req.Body.Close() 23 | 24 | resp.Write(reqBody) 25 | } 26 | -------------------------------------------------------------------------------- /https/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | http.HandleFunc("/", echo) 11 | log.Fatal(http.ListenAndServeTLS("localhost:4430", 12 | "localhost.pem", "localhost-key.pem", nil)) 13 | } 14 | 15 | func echo(resp http.ResponseWriter, req *http.Request) { 16 | reqBody, err := io.ReadAll(req.Body) 17 | if err != nil { 18 | http.Error(resp, 19 | "Error reading request body", 20 | http.StatusInternalServerError) 21 | return 22 | } 23 | defer req.Body.Close() 24 | 25 | resp.Write(reqBody) 26 | } 27 | -------------------------------------------------------------------------------- /tls/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "log" 7 | "net" 8 | ) 9 | 10 | func main() { 11 | cert, err := tls.LoadX509KeyPair("localhost.pem", "localhost-key.pem") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | config := &tls.Config{ 17 | Certificates: []tls.Certificate{cert}, 18 | } 19 | 20 | ln, err := tls.Listen("tcp", "localhost:4321", config) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer ln.Close() 25 | 26 | for { 27 | conn, err := ln.Accept() 28 | if err != nil { 29 | log.Print(err) 30 | continue 31 | } 32 | go echo(conn) 33 | } 34 | } 35 | 36 | func echo(conn net.Conn) { 37 | n, err := io.Copy(conn, conn) 38 | log.Printf("sent %d bytes to %s, err: %v", n, conn.RemoteAddr(), err) 39 | conn.Close() 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | I think it's still important to think even though we have the generative AI now. Thinking always has been and always will be good and useful. Before we can start thinking more deeply about something we have to learn the basics. And one of the best ways how to learn anything is by doing it. Playing is a way of doing. You should probably not play in production though. 2 | 3 | Take TLS for example. It used to be called SSL before and it's the protocol that secures the network communication. It sits between Application and Transport layers in a networking model: 4 | 5 | ``` 6 | TCP/IP Layer | Protocol / Medium 7 | --------------- | ---------------------- 8 | Application | HTTP, SMTP, DNS, ... 9 | Transport | TCP, UDP 10 | Internet | IP 11 | Link / Network | Ethernet, WiFi 12 | Physical | Cables, radio, optical 13 | ``` 14 | 15 | (This is the TCP/IP networking model which is simpler than the OSI model. However, the OSI model layers are not that hard to remember, just Please Do Not Throw The Sausage Pizza Away :-) 16 | 17 | Now, let's play with it a bit instead of just reading the theory. 18 | 19 | ## TCP 20 | 21 | TLS wraps application data (like HTTP) before it goes over TCP. Therefore let's have a look at how TCP works first. We build a simple TCP server: 22 | 23 | ```go 24 | // ./tcp/server.go 25 | func main() { 26 | ln, err := net.Listen("tcp", "localhost:1234") 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer ln.Close() 31 | 32 | for { 33 | conn, err := ln.Accept() 34 | if err != nil { 35 | log.Print(err) 36 | continue 37 | } 38 | go echo(conn) 39 | } 40 | } 41 | 42 | func echo(conn net.Conn) { 43 | io.Copy(conn, conn) 44 | conn.Close() 45 | } 46 | ``` 47 | 48 | The server echoes back anything a client sends to it: 49 | 50 | ``` 51 | $ go run ./tcp/server.go & 52 | $ nc localhost 1234 # connect to the server 53 | hello # sent data 54 | hello # received data 55 | ``` 56 | 57 | The data (`hello\n`) goes over the network in plaintext. If someone eavesdrops on the network, for example using Wireshark, they see the transferred data: 58 | 59 | image 60 | 61 | ## TLS 62 | 63 | TLS encrypts (and provides identity and integrity of) the application data so that it's not readable when someone gets it from the network. We'll use the [crypto/tls](https://pkg.go.dev/crypto/tls) standard library package instead of the [net](https://pkg.go.dev/net) we used above. TLS is based on asymmetric encryption that uses different key for encryption and decryption: 64 | 65 | image 66 | 67 | In our case Bob is the server running on localhost and Alice is the client (like `nc` or `openssl`) talking to the server. Eve is the guy on snooping on the network with Wireshark. So we'll need to supply a TLS certificate - basically the public key (`localhost.pem`) and private key (`localhost-key.pem`) as configuration to our server: 68 | 69 | ```go 70 | // ./tls/server.go 71 | cert, err := tls.LoadX509KeyPair("localhost.pem", "localhost-key.pem") 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | config := &tls.Config{ 77 | Certificates: []tls.Certificate{cert}, 78 | } 79 | 80 | ln, err := tls.Listen("tcp", "localhost:4321", config) 81 | // The rest of the code is as above... 82 | ``` 83 | 84 | I added logging to the `echo` function so we can see what's going on with the connection: 85 | 86 | ```go 87 | // ./tls/server.go 88 | func echo(conn net.Conn) { 89 | n, err := io.Copy(conn, conn) 90 | log.Printf("sent %d bytes to %s, err: %v", n, conn.RemoteAddr(), err) 91 | conn.Close() 92 | } 93 | ``` 94 | 95 | I use the [mkcert](https://github.com/FiloSottile/mkcert) tool to create the certificate and the private key files for localhost: 96 | 97 | ``` 98 | $ mkcert localhost 99 | ``` 100 | 101 | But when we send some data to the server now, we don't see anything echoed back: 102 | 103 | ``` 104 | $ go run ./tls/server.go & 105 | $ nc localhost 4321 # connect to the server 106 | hello # sent data 107 | ``` 108 | 109 | Let's have a look at the server logs: 110 | 111 | ``` 112 | 2025/09/25 18:00:06 sent 0 bytes to 127.0.0.1:52130, err: tls: first record does not look like a TLS handshake 113 | ``` 114 | 115 | Yes, netcat (`nc`) can't speak TLS - the yellow steps below (the TLS handshake). It can only speak TCP - the blue steps (the standard three-way TCP handshake): 116 | 117 | image 118 | 119 | But `openssl` can: 120 | 121 | ``` 122 | $ openssl s_client -connect localhost:4321 -servername localhost -quiet 2> /dev/null 123 | hello 124 | hello 125 | ``` 126 | 127 | If Eve captures the data from the network now, they can't read it since it's encrypted. 128 | 129 | ## HTTP 130 | 131 | TLS is often used to secure the HTTP protocol. HTTP on top of TLS becomes HTTPS. 132 | 133 | So let's start with HTTP. Here's the HTTP version of the echo server from the above: 134 | 135 | ```go 136 | // ./http/server.go 137 | func main() { 138 | http.HandleFunc("/", echo) 139 | log.Fatal(http.ListenAndServe("localhost:8080", nil)) 140 | } 141 | 142 | func echo(resp http.ResponseWriter, req *http.Request) { 143 | reqBody, err := io.ReadAll(req.Body) 144 | if err != nil { 145 | http.Error(resp, 146 | "Error reading request body", 147 | http.StatusInternalServerError) 148 | return 149 | } 150 | defer req.Body.Close() 151 | 152 | resp.Write(reqBody) 153 | } 154 | ``` 155 | 156 | Let's start the server and send some data to it via `curl`: 157 | 158 | ``` 159 | $ go run http/server.go & 160 | $ curl localhost:8080 --data hello 161 | hello 162 | ``` 163 | 164 | We see it gets echoed back. In plaintext, unencrypted. This makes Eve very happy. 165 | 166 | ## HTTPS 167 | 168 | To secure our HTTP communication we just need to use `http.ListenAndServeTLS` instead of `http.ListenAndServe` (both are part the powerful [net/http](https://pkg.go.dev/net/http) stdlib package). And we need to supply files containing a certificate and matching private key: 169 | 170 | ```go 171 | // ./https/server.go 172 | http.HandleFunc("/", echo) 173 | log.Fatal(http.ListenAndServeTLS("localhost:4430", 174 | "localhost.pem", "localhost-key.pem", nil)) 175 | ``` 176 | 177 | On the client side, we need to specify `https://` in the URL (`curl` defaults to `http://`) and port `4430` instead of `8080`. Also, since our certificate is not signed by a trusted certificate authority (CA) that is pre-installed in browser/OS we either skip the server certificate verification or we supply the server certificate file: 178 | 179 | ``` 180 | $ go run https/server.go & 181 | 182 | # skip the server certificate verification 183 | $ curl https://localhost:4430 --data hello --insecure 184 | hello 185 | 186 | # supply the server certificate file 187 | $ curl https://localhost:4430 --data hello --cacert localhost.pem 188 | hello 189 | ``` 190 | 191 | Eve gets somehow sad now ... 192 | --------------------------------------------------------------------------------