2022-05-22 00:00:00 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2022-05-26 00:00:00 +00:00
|
|
|
"log"
|
2022-05-22 00:00:00 +00:00
|
|
|
"net"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
type tlsConnectionInfo struct {
|
|
|
|
TlsVersion uint16 `json:"tls_version"`
|
|
|
|
CipherSuite uint16 `json:"cipher_suite"`
|
|
|
|
SessionResumed bool `json:"session_resumed"`
|
|
|
|
NegotiatedProtocol string `json:"alpn_negotiated_protocol"` // ALPN
|
|
|
|
}
|
|
|
|
|
|
|
|
type prefixConn struct {
|
|
|
|
net.Conn
|
|
|
|
io.Reader
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c prefixConn) Read(p []byte) (int, error) {
|
|
|
|
return c.Reader.Read(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
const html = `<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
<meta name="theme-color" content="#000">
|
|
|
|
<meta name="referrer" content="no-referrer">
|
|
|
|
<title>TLS Client Hello Mirror</title>
|
|
|
|
<style>
|
|
|
|
:root {
|
|
|
|
color-scheme: dark;
|
|
|
|
}
|
|
|
|
::selection {
|
|
|
|
color: #FFF;
|
|
|
|
background-color: #070;
|
|
|
|
}
|
|
|
|
body {
|
|
|
|
color: #DDD;
|
|
|
|
background-color: #000;
|
|
|
|
margin: 1em auto;
|
|
|
|
max-width: 38em;
|
|
|
|
padding: 0 .62em;
|
|
|
|
font: 1.1em/1.62 sans-serif;
|
|
|
|
}
|
|
|
|
@media print{
|
|
|
|
body{
|
|
|
|
max-width: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
a:link {color:#EEE;}
|
|
|
|
a:visited {color:#EEE;}
|
|
|
|
a:hover {color:#FFF;}
|
|
|
|
a:active {color:#FFF;}
|
|
|
|
</style>
|
|
|
|
<main>
|
|
|
|
<center>
|
|
|
|
<h1>TLS Client Hello Mirror</h1>
|
|
|
|
</center>
|
|
|
|
<h3><a href="/json/v1">Your browser's TLS Client Hello, reflected as JSON</a></h3>
|
|
|
|
<p>This test:
|
|
|
|
<ul>
|
|
|
|
<li>reflects the complete Client Hello message, preserving the order in which TLS parameters and extensions are sent;</li>
|
2022-05-27 00:00:00 +00:00
|
|
|
<li>can be used to check for TLS privacy pitfalls (<a href="https://svs.informatik.uni-hamburg.de/publications/2018/2018-12-06-Sy-ACSAC-Tracking_Users_across_the_Web_via_TLS_Session_Resumption.pdf">session resumption</a>, <a href="https://tlsfingerprint.io/">TLS fingerprinting</a>, <a href="https://datatracker.ietf.org/doc/html/draft-mathewson-no-gmtunixtime">system time exposure</a>);</li>
|
2022-05-22 00:00:00 +00:00
|
|
|
<li>supports both HTTP and <a href="https://gemini.circumlunar.space/">Gemini</a>;</li>
|
|
|
|
<li>is <a href="https://www.gnu.org/philosophy/free-sw.en.html">free as in freedom</a> and trivial to self-host.</li>
|
|
|
|
</ul>
|
|
|
|
</p>
|
|
|
|
<p>JSON only, for now, but a UI is on <a href="https://tildegit.org/nervuri/client-hello-mirror#roadmap">the roadmap</a>.</p>
|
|
|
|
<p>The API is largely stable - fields may be added, but existing fields will not be modified or removed. IANA-assigned codes for TLS parameters and extensions are documented at:
|
|
|
|
<ul>
|
|
|
|
<li><a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml">TLS parameters</a></li>
|
|
|
|
<li><a href="https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml">TLS extensions</a></li>
|
|
|
|
</ul>
|
|
|
|
Note that these lists do not include draft extensions and <a href="https://datatracker.ietf.org/doc/html/rfc8701">GREASE</a> values. Missing values will be documented here as the project evolves.
|
|
|
|
</p>
|
|
|
|
</main>
|
|
|
|
<hr>
|
|
|
|
<footer>
|
2022-05-26 00:00:00 +00:00
|
|
|
Author: <a href="https://nervuri.net/">nervuri</a><br>
|
2022-05-22 00:00:00 +00:00
|
|
|
<a href="https://tildegit.org/nervuri/client-hello-mirror">Source</a> (contributions welcome)<br>
|
|
|
|
License: <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL-3.0-or-later</a>
|
|
|
|
</footer>
|
|
|
|
</html>`
|
|
|
|
|
|
|
|
const gemtext = `# TLS Client Hello Mirror
|
|
|
|
|
|
|
|
=> /json/v1 Your browser's TLS Client Hello, reflected as JSON
|
|
|
|
|
|
|
|
This test:
|
|
|
|
|
|
|
|
* reflects the complete Client Hello message, preserving the order in which TLS parameters and extensions are sent;
|
|
|
|
* can be used to check for TLS privacy pitfalls (session resumption, TLS fingerprinting, system time exposure);
|
|
|
|
* supports multiple protocols (currently HTTP and Gemini);
|
|
|
|
* is free as in freedom and trivial to self-host.
|
|
|
|
|
|
|
|
JSON only, for now. The API is largely stable - fields may be added, but existing fields will not be modified or removed. IANA-assigned codes for TLS parameters and extensions are available at:
|
|
|
|
|
|
|
|
=> https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml TLS parameters
|
|
|
|
=> https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml TLS extensions
|
|
|
|
|
|
|
|
Note that these lists do not include draft extensions and GREASE values. Missing values will be documented here as the project evolves.
|
|
|
|
|
|
|
|
_____________________
|
2022-05-26 00:00:00 +00:00
|
|
|
=> https://nervuri.net/ Author: nervuri
|
2022-05-22 00:00:00 +00:00
|
|
|
=> https://tildegit.org/nervuri/client-hello-mirror Source (contributions welcome)
|
|
|
|
=> https://www.gnu.org/licenses/agpl-3.0.en.html License: AGPL-3.0-or-later`
|
|
|
|
|
2022-06-12 00:00:00 +00:00
|
|
|
// Copy the Client Hello message before starting the TLS handshake.
|
2022-05-22 00:00:00 +00:00
|
|
|
func peek(conn net.Conn, tlsConfig *tls.Config) {
|
|
|
|
defer conn.Close()
|
|
|
|
var buf bytes.Buffer
|
2022-06-12 00:00:00 +00:00
|
|
|
// Copy TLS record header.
|
|
|
|
_, err := io.CopyN(&buf, conn, 5)
|
2022-05-22 00:00:00 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
2022-06-12 00:00:00 +00:00
|
|
|
// Check if this is a TLS handshake record.
|
2022-06-04 00:00:00 +00:00
|
|
|
if buf.Bytes()[0] != 0x16 {
|
|
|
|
return
|
|
|
|
}
|
2022-06-12 00:00:00 +00:00
|
|
|
// Extract handshake message length.
|
|
|
|
handshakeMessageLength := binary.BigEndian.Uint16(buf.Bytes()[3:5])
|
|
|
|
// Copy handshake message (should be a Client Hello).
|
|
|
|
_, err = io.CopyN(&buf, conn, int64(handshakeMessageLength))
|
2022-05-22 00:00:00 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rawClientHello := buf.Bytes()
|
|
|
|
|
|
|
|
pConn := prefixConn{
|
|
|
|
Conn: conn,
|
|
|
|
Reader: io.MultiReader(&buf, conn),
|
|
|
|
}
|
|
|
|
|
|
|
|
server := tls.Server(pConn, tlsConfig)
|
|
|
|
err = server.Handshake()
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tlsHandler(server, rawClientHello)
|
|
|
|
}
|
|
|
|
|
|
|
|
func tlsHandler(conn *tls.Conn, rawClientHello []byte) {
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
scanner.Scan()
|
|
|
|
line := scanner.Text()
|
|
|
|
err := scanner.Err()
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
//log.Println(line)
|
|
|
|
|
|
|
|
var protocol string
|
|
|
|
var path string // requested page
|
|
|
|
|
|
|
|
// Detect protocol by checking the first few characters.
|
|
|
|
if strings.HasPrefix(line, "gemini://") {
|
|
|
|
protocol = "Gemini"
|
|
|
|
u, err := url.Parse(line)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
path = u.Path
|
|
|
|
if path == "" {
|
|
|
|
path = "/"
|
|
|
|
}
|
|
|
|
if path == "/" {
|
|
|
|
_, err = conn.Write([]byte("20 text/gemini\r\n"))
|
|
|
|
} else if path == "/json/v1" {
|
|
|
|
_, err = conn.Write([]byte("20 application/json\r\n"))
|
|
|
|
}
|
|
|
|
} else if strings.HasPrefix(line, "GET ") ||
|
2022-05-26 00:00:00 +00:00
|
|
|
strings.HasPrefix(line, "POST ") ||
|
|
|
|
strings.HasPrefix(line, "HEAD ") {
|
2022-05-22 00:00:00 +00:00
|
|
|
protocol = "HTTP"
|
|
|
|
path = strings.Split(line, " ")[1]
|
|
|
|
if path == "/" {
|
|
|
|
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\n"))
|
|
|
|
_, err = conn.Write([]byte("Content-Type: text/html; charset=utf-8\r\n"))
|
|
|
|
_, err = conn.Write([]byte("Cache-Control: no-store\r\n"))
|
|
|
|
_, err = conn.Write([]byte("Vary: *\r\n"))
|
|
|
|
} else if path == "/json/v1" {
|
|
|
|
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\n"))
|
|
|
|
_, err = conn.Write([]byte("Content-Type: application/json\r\n"))
|
|
|
|
_, err = conn.Write([]byte("Cache-Control: no-store\r\n"))
|
|
|
|
_, err = conn.Write([]byte("Vary: *\r\n"))
|
|
|
|
} else {
|
|
|
|
_, err = conn.Write([]byte("HTTP/1.1 404 Not Found\r\n"))
|
|
|
|
}
|
|
|
|
_, err = conn.Write([]byte("\r\n")) // end headers
|
|
|
|
if strings.HasPrefix(line, "HEAD ") {
|
|
|
|
return // headers only
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.Println("Unknown protocol")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var clientHello clientHelloMsg
|
|
|
|
if !clientHello.unmarshal(rawClientHello) {
|
|
|
|
log.Println("Failed to parse Client Hello")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
connectionState := conn.ConnectionState() // TLS
|
|
|
|
tlsConnInfo := tlsConnectionInfo{
|
|
|
|
connectionState.Version,
|
|
|
|
connectionState.CipherSuite,
|
|
|
|
connectionState.DidResume,
|
|
|
|
connectionState.NegotiatedProtocol,
|
|
|
|
}
|
|
|
|
|
|
|
|
if path == "/" {
|
|
|
|
if protocol == "HTTP" {
|
|
|
|
_, err = conn.Write([]byte(html))
|
|
|
|
} else if protocol == "Gemini" {
|
|
|
|
_, err = conn.Write([]byte(gemtext))
|
|
|
|
} else {
|
|
|
|
log.Println("Unknown protocol")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else if path == "/json/v1" {
|
|
|
|
output := struct {
|
2022-05-26 00:00:00 +00:00
|
|
|
ClientHello clientHelloMsg `json:"client_hello"`
|
2022-05-22 00:00:00 +00:00
|
|
|
TlsConnectionInfo tlsConnectionInfo `json:"connection_info"`
|
|
|
|
}{
|
|
|
|
clientHello,
|
|
|
|
tlsConnInfo,
|
|
|
|
}
|
|
|
|
outputJSON, err := json.MarshalIndent(output, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
_, err = conn.Write(outputJSON)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
|
|
var certFile, keyFile string
|
2022-05-26 00:00:00 +00:00
|
|
|
var userToSwitchTo string
|
2022-05-22 00:00:00 +00:00
|
|
|
var hostAndPort string
|
|
|
|
|
|
|
|
// Parse arguments
|
|
|
|
flag.StringVar(&certFile, "c", "", "path to certificate file")
|
|
|
|
flag.StringVar(&keyFile, "k", "", "path to private key file")
|
2023-01-31 15:23:14 +00:00
|
|
|
flag.StringVar(&userToSwitchTo, "u", "", "user to switch to, if running as root")
|
2022-05-22 00:00:00 +00:00
|
|
|
flag.Parse()
|
|
|
|
hostAndPort = flag.Arg(0)
|
2022-05-26 00:00:00 +00:00
|
|
|
if certFile == "" || keyFile == "" || hostAndPort == "" {
|
2023-01-31 15:23:14 +00:00
|
|
|
fmt.Println("usage: client-hello-mirror -c cert.pem -k key.pem [-u user] host:port")
|
2022-05-22 00:00:00 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load cert
|
|
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// TLS config
|
|
|
|
tlsConfig := tls.Config{
|
|
|
|
Certificates: []tls.Certificate{cert},
|
|
|
|
//MaxVersion: tls.VersionTLS12,
|
2022-06-04 00:00:00 +00:00
|
|
|
NextProtos: []string{"http/1.1"},
|
2022-05-22 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 00:00:00 +00:00
|
|
|
// Listen for connections
|
|
|
|
ln, err := net.Listen("tcp", hostAndPort)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer ln.Close()
|
|
|
|
|
2023-02-15 14:44:59 +00:00
|
|
|
dropPrivileges(userToSwitchTo)
|
2022-05-26 00:00:00 +00:00
|
|
|
|
2022-05-22 00:00:00 +00:00
|
|
|
log.Println("Server started")
|
|
|
|
|
|
|
|
for {
|
|
|
|
conn, err := ln.Accept()
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Error accepting: ", err.Error())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
conn.SetReadDeadline(time.Now().Add(5 * time.Second)) // timeout
|
|
|
|
go peek(conn, &tlsConfig)
|
|
|
|
}
|
|
|
|
}
|