tlsparse

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package tlsparse decodes TLS handshake messages from raw bytes.

Everything in this package is a pure function over a byte slice: no I/O, no network access, no global state, and no dependency beyond golang.org/x/crypto/cryptobyte. That is deliberate. This is the code that reads attacker-controlled input inside a privileged process, so it is also the code that has to be trivially fuzzable, auditable in isolation, and testable without a capture device.

Parsers here are tolerant of truncation by design. A capture keeps only a bounded prefix of each stream, so a ServerHello may legitimately arrive cut in half. Parsers return what they could decode rather than failing the whole flow, and callers decide whether a partial record is useful.

Index

Constants

View Source
const (
	ExtServerName              uint16 = 0
	ExtMaxFragmentLength       uint16 = 1
	ExtStatusRequest           uint16 = 5
	ExtSupportedGroups         uint16 = 10
	ExtECPointFormats          uint16 = 11
	ExtSignatureAlgorithms     uint16 = 13
	ExtUseSRTP                 uint16 = 14
	ExtHeartbeat               uint16 = 15
	ExtALPN                    uint16 = 16
	ExtSignedCertTimestamp     uint16 = 18
	ExtClientCertType          uint16 = 19
	ExtServerCertType          uint16 = 20
	ExtPadding                 uint16 = 21
	ExtEncryptThenMAC          uint16 = 22
	ExtExtendedMasterSecret    uint16 = 23
	ExtCompressCertificate     uint16 = 27
	ExtRecordSizeLimit         uint16 = 28
	ExtDelegatedCredential     uint16 = 34
	ExtSessionTicket           uint16 = 35
	ExtPreSharedKey            uint16 = 41
	ExtEarlyData               uint16 = 42
	ExtSupportedVersions       uint16 = 43
	ExtCookie                  uint16 = 44
	ExtPSKKeyExchangeModes     uint16 = 45
	ExtCertificateAuthorities  uint16 = 47
	ExtPostHandshakeAuth       uint16 = 49
	ExtSignatureAlgorithmsCert uint16 = 50
	ExtKeyShare                uint16 = 51
	ExtQUICTransportParams     uint16 = 57
	ExtNextProtocolNegotiation uint16 = 13172
	ExtApplicationSettings     uint16 = 17513
	ExtApplicationSettingsOld  uint16 = 17613
	ExtEncryptedClientHello    uint16 = 0xfe0d
	ExtRenegotiationInfo       uint16 = 0xff01
)

Extension type codepoints.

View Source
const (
	RecordChangeCipherSpec uint8 = 20
	RecordAlert            uint8 = 21
	RecordHandshake        uint8 = 22
	RecordApplicationData  uint8 = 23
)

TLS record content types (RFC 8446 section 5.1).

View Source
const (
	HandshakeClientHello         uint8 = 1
	HandshakeServerHello         uint8 = 2
	HandshakeNewSessionTicket    uint8 = 4
	HandshakeEncryptedExtensions uint8 = 8
	HandshakeCertificate         uint8 = 11
	HandshakeServerKeyExchange   uint8 = 12
	HandshakeCertificateRequest  uint8 = 13
	HandshakeServerHelloDone     uint8 = 14
)

Handshake message types (RFC 8446 appendix B.3).

View Source
const (
	GroupSourceKeyShare          = "key_share"
	GroupSourceServerKeyExchange = "server_key_exchange"
)

Key exchange sources, reported in ServerHello.GroupSource.

Variables

View Source
var (
	// ErrNotTLS means the bytes do not begin with a plausible TLS handshake
	// record. Callers use this to reject non-TLS flows cheaply.
	ErrNotTLS = errors.New("tlsparse: not a TLS handshake record")
	// ErrTruncated means the message ended before a required field.
	ErrTruncated = errors.New("tlsparse: truncated message")
	// ErrMalformed means a length prefix was internally inconsistent.
	ErrMalformed = errors.New("tlsparse: malformed message")
)

Functions

func CipherByName

func CipherByName(name string) (uint16, bool)

CipherByName returns the codepoint for an IANA cipher suite name.

func CipherName

func CipherName(id uint16) string

CipherName returns the IANA name for a cipher suite codepoint, or its hex form when unrecognised.

func ExtensionName

func ExtensionName(id uint16) string

ExtensionName returns the registered name for an extension codepoint, or its hex form when unrecognised.

func GroupByName

func GroupByName(name string) (uint16, bool)

GroupByName returns the codepoint for a named group.

func GroupName

func GroupName(id uint16) string

GroupName returns the name for a named-group codepoint, or its hex form.

func IsGREASE

func IsGREASE(v uint16) bool

IsGREASE reports whether v is a GREASE value as defined by RFC 8701.

The sixteen reserved values are 0x0a0a, 0x1a1a, ... 0xfafa: both bytes are equal and both low nibbles are 0xa. Clients (notably BoringSSL) inject them into cipher suite, extension, named group, ALPN and signature algorithm lists to keep servers tolerant of unknown values.

An inventory that does not strip them reports phantom "unknown cipher 0x8a8a" rows for a large share of real-world traffic, so every list this package exposes is GREASE-free by construction. Whether GREASE was observed at all is recorded separately on the ClientHello, since its presence is itself a fingerprinting signal.

func ParseCertificateChain

func ParseCertificateChain(body []byte) ([][]byte, bool)

ParseCertificateChain extracts the raw DER certificates from a TLS 1.2 Certificate message. TLS 1.3 uses a different encoding and encrypts the message regardless, so this only ever runs on 1.2 and below.

func ParseServerKeyExchangeGroup

func ParseServerKeyExchangeGroup(body []byte) (group uint16, ok bool)

ParseServerKeyExchangeGroup extracts the named curve from a TLS 1.2 ECDHE ServerKeyExchange message.

This is the only place a TLS 1.2 handshake states its key exchange group, since there is no key_share extension before TLS 1.3. Without it every TLS 1.2 flow would report an unknown group, which would make the classical-versus-post-quantum split meaningless on exactly the traffic most likely to be classical.

Non-ECDHE key exchanges (plain DHE, static RSA) have a different body shape and report ok=false.

func SigAlgName

func SigAlgName(id uint16) string

SigAlgName returns the name for a signature-algorithm codepoint, or its hex form.

func SigAlgPostQuantum

func SigAlgPostQuantum(id uint16) bool

SigAlgPostQuantum reports whether a signature algorithm is post-quantum.

func VersionName

func VersionName(v uint16) string

VersionName returns a human-readable protocol version.

Types

type CipherProperties

type CipherProperties struct {
	Name string `json:"name"`
	// KeyExchange is "ECDHE", "DHE", "RSA", "DH_anon", ... or "" for TLS 1.3
	// suites, where key exchange is negotiated by extension instead.
	KeyExchange string `json:"key_exchange,omitempty"`
	// Authentication is the signature algorithm family implied by the suite:
	// "RSA", "ECDSA", "DSS", "anon", or "" for TLS 1.3.
	Authentication string `json:"authentication,omitempty"`
	Encryption     string `json:"encryption"`
	MAC            string `json:"mac"`
	ForwardSecrecy bool   `json:"forward_secrecy"`
	AEAD           bool   `json:"aead"`
	Export         bool   `json:"export,omitempty"`
	Anonymous      bool   `json:"anonymous,omitempty"`
	TLS13          bool   `json:"tls13,omitempty"`
	Signalling     bool   `json:"signalling,omitempty"`
	// Known is false when the codepoint is not in the table; Name is then
	// the hex form and every other field is zero.
	Known bool `json:"known"`
}

CipherProperties describes a cipher suite. Fields are derived from the structure of the IANA name, which is rigidly formatted as TLS_<kx>_WITH_<cipher>_<mac> for TLS 1.2 and below, and TLS_<aead>_<hash> for TLS 1.3.

func Cipher

func Cipher(id uint16) CipherProperties

Cipher returns the properties of a cipher suite codepoint.

type ClientHello

type ClientHello struct {
	LegacyVersion      uint16   `json:"legacy_version"`
	SessionIDLen       int      `json:"session_id_len"`
	CipherSuites       []uint16 `json:"cipher_suites"`
	CompressionMethods []uint8  `json:"compression_methods"`
	// Extensions lists extension codepoints in the order they appeared.
	Extensions []uint16 `json:"extensions"`

	ServerName        string   `json:"server_name,omitempty"`
	ALPN              []string `json:"alpn,omitempty"`
	SupportedVersions []uint16 `json:"supported_versions,omitempty"`
	SupportedGroups   []uint16 `json:"supported_groups,omitempty"`

	// KeyShareGroups are the groups the client actually sent a key share
	// for, in offer order.
	//
	// This is not the same signal as SupportedGroups and the difference is
	// the whole point of a PQ-readiness inventory. SupportedGroups says what
	// the client would accept; KeyShareGroups says what it spent bytes
	// betting on. A client that advertises X25519MLKEM768 in supported
	// groups but only key-shares x25519 will complete a classical handshake
	// against any server that takes the offer, and is not post-quantum in
	// practice.
	KeyShareGroups []uint16 `json:"key_share_groups,omitempty"`

	SignatureAlgorithms     []uint16 `json:"signature_algorithms,omitempty"`
	SignatureAlgorithmsCert []uint16 `json:"signature_algorithms_cert,omitempty"`
	PSKKeyExchangeModes     []uint8  `json:"psk_key_exchange_modes,omitempty"`

	HasPreSharedKey         bool `json:"has_pre_shared_key,omitempty"`
	HasEarlyData            bool `json:"has_early_data,omitempty"`
	HasSessionTicket        bool `json:"has_session_ticket,omitempty"`
	HasExtendedMasterSecret bool `json:"has_extended_master_secret,omitempty"`
	HasRenegotiationInfo    bool `json:"has_renegotiation_info,omitempty"`
	HasStatusRequest        bool `json:"has_status_request,omitempty"`
	// QUIC is true when quic_transport_parameters is present, which marks
	// this as a QUIC handshake rather than TLS over TCP. It feeds the
	// protocol character of JA4.
	QUIC bool `json:"quic,omitempty"`

	// ECH is set when an encrypted_client_hello extension was present. When
	// it is, ServerName is the public outer name, not the real destination.
	ECH *ECHInfo `json:"ech,omitempty"`

	// GREASE reports whether any GREASE codepoint was observed. Its absence
	// is itself informative: most modern browsers always send it.
	GREASE bool `json:"grease"`

	// Truncated is true when the extension block ran past the available
	// bytes. Fields decoded before that point are still valid.
	Truncated bool `json:"truncated,omitempty"`
}

ClientHello is the decoded content of a TLS ClientHello.

All codepoint lists are GREASE-free (RFC 8701) and preserve wire order, which JA4 depends on. Whether GREASE was present at all is recorded in the GREASE field.

func FindClientHello

func FindClientHello(stream []byte) (*ClientHello, error)

FindClientHello returns the first ClientHello in a record-framed client-to-server byte stream, or nil if there is none.

func FindClientHelloRaw

func FindClientHelloRaw(stream []byte) (*ClientHello, error)

FindClientHelloRaw is FindClientHello for a stream with no record layer, as QUIC CRYPTO frames carry.

func ParseClientHello

func ParseClientHello(body []byte) (*ClientHello, error)

ParseClientHello decodes a ClientHello handshake message body, excluding the four-byte handshake header.

func (*ClientHello) JA4

func (ch *ClientHello) JA4() string

JA4 returns the JA4 client fingerprint, in the form "t13d1516h2_8daaf6152771_b186095e22b6".

func (*ClientHello) NegotiatedVersion

func (ch *ClientHello) NegotiatedVersion() uint16

NegotiatedVersion returns the highest protocol version the client offered: the maximum of supported_versions when present, otherwise the legacy version field. GREASE values are already excluded.

type ECHInfo

type ECHInfo struct {
	// Outer is true for ClientHelloOuter (the handshake visible on the
	// wire), false for ClientHelloInner (only seen after decryption, so
	// effectively never in passive capture).
	Outer    bool   `json:"outer"`
	KDFID    uint16 `json:"kdf_id,omitempty"`
	AEADID   uint16 `json:"aead_id,omitempty"`
	ConfigID uint8  `json:"config_id,omitempty"`
}

ECHInfo records what an encrypted_client_hello extension reveals.

This matters more than it looks. When ECH is in use the SNI visible on the wire is the public "outer" name of the provider, not the host the client actually asked for. An inventory that records it as the destination is not degraded, it is wrong, so every observation carries this flag and consumers are expected to report ECH flows separately rather than folding them into hostname counts.

type GroupProperties

type GroupProperties struct {
	Name string `json:"name"`
	// PostQuantum is true when the group contributes a post-quantum KEM to
	// the shared secret, whether or not it is hybrid.
	PostQuantum bool `json:"post_quantum"`
	// Hybrid is true when a classical and a post-quantum component are
	// combined. Hybrid is the deployable form today; a pure KEM group means
	// classical security has been dropped entirely.
	Hybrid bool `json:"hybrid,omitempty"`
	Known  bool `json:"known"`
}

GroupProperties describes a named group offered or selected for key exchange.

func Group

func Group(id uint16) GroupProperties

Group returns the properties of a named-group codepoint.

type HandshakeMessage

type HandshakeMessage struct {
	Type uint8
	Body []byte
}

HandshakeMessage is one handshake message lifted out of the record layer. Body excludes the four-byte type and length header.

func HandshakeMessages

func HandshakeMessages(b []byte) ([]HandshakeMessage, error)

HandshakeMessages coalesces the payloads of the handshake records in b and splits the result into messages.

Coalescing matters: a handshake message may be fragmented across several records, and a record may carry several messages. Parsers that assume one message per record work on most traffic and then fail on exactly the handshakes worth reporting.

Reading stops at the first application-data record, since under TLS 1.3 every handshake message after ServerHello is encrypted inside those.

func HandshakeMessagesRaw

func HandshakeMessagesRaw(b []byte) []HandshakeMessage

HandshakeMessagesRaw splits a bare handshake stream — one with no record layer around it.

QUIC does not use the TLS record layer at all: CRYPTO frames carry handshake messages directly (RFC 9001 section 4). Feeding those bytes to HandshakeMessages looks for a record header that is not there and rejects the whole flow as non-TLS, which is a silent and total loss rather than a visible error.

type ServerHello

type ServerHello struct {
	LegacyVersion     uint16   `json:"legacy_version"`
	CipherSuite       uint16   `json:"cipher_suite"`
	CompressionMethod uint8    `json:"compression_method"`
	Extensions        []uint16 `json:"extensions"`

	// SelectedVersion is the supported_versions value when present. It is
	// authoritative over LegacyVersion, which a TLS 1.3 server pins to
	// TLS 1.2 for middlebox compatibility.
	SelectedVersion uint16 `json:"selected_version,omitempty"`

	// Group is the negotiated key exchange group, and GroupSource records
	// where it was read from.
	Group       uint16 `json:"group,omitempty"`
	GroupSource string `json:"group_source,omitempty"`

	SelectedALPN string `json:"selected_alpn,omitempty"`

	IsHelloRetryRequest bool `json:"is_hello_retry_request,omitempty"`
	HasPreSharedKey     bool `json:"has_pre_shared_key,omitempty"`

	// FlightComplete reports that the server has said everything it will
	// ever say in the clear, so nothing is gained by waiting for more
	// bytes on this connection.
	//
	// Under TLS 1.3 that is the ServerHello itself: Certificate,
	// CertificateVerify and EncryptedExtensions all move under handshake
	// encryption. Under TLS 1.2 it is ServerHelloDone, which closes the
	// server's flight after Certificate and ServerKeyExchange.
	//
	// It exists so an observation can be reported as soon as it is
	// complete rather than when the connection closes. A browser holds
	// connections open for minutes, so waiting for the close means a live
	// capture shows nothing for minutes after the handshake it just saw.
	//
	// A resumed TLS 1.2 session sends no ServerHelloDone — the server goes
	// straight to ChangeCipherSpec — so those stay false and are reported
	// when the connection ends. That is a latency cost on an uncommon case,
	// not a loss.
	FlightComplete bool `json:"flight_complete,omitempty"`

	// CertificateChain holds the raw DER of the server's certificate chain,
	// leaf first. Populated for TLS 1.2 and below only.
	CertificateChain [][]byte `json:"-"`

	Truncated bool `json:"truncated,omitempty"`
}

ServerHello is the decoded content of a TLS ServerHello, enriched with what the surrounding cleartext handshake messages reveal.

Under TLS 1.3 that is everything the server will ever disclose in the clear: Certificate, CertificateVerify and EncryptedExtensions all move under handshake encryption. Certificate inventory — key type, key size, issuer, validity — is therefore only available for TLS 1.2 and below. This is a property of the protocol, not a gap in this parser.

func FindServerHello

func FindServerHello(stream []byte) (*ServerHello, error)

FindServerHello returns the first real ServerHello in a server-to-client byte stream, or nil if there is none.

The result is enriched from the messages that follow it in the clear. For TLS 1.2 that means the ServerKeyExchange group and the certificate chain; for TLS 1.3 those messages are encrypted and nothing further is available.

A HelloRetryRequest is encoded as a ServerHello. When one is present the scan keeps going, because the client answers it with a second ClientHello and the server with the real ServerHello, both still in the clear. The HRR is only returned if no real ServerHello follows it in the captured prefix.

func FindServerHelloRaw

func FindServerHelloRaw(stream []byte) (*ServerHello, error)

FindServerHelloRaw is FindServerHello for a stream with no record layer, as QUIC CRYPTO frames carry.

func ParseServerHello

func ParseServerHello(body []byte) (*ServerHello, error)

ParseServerHello decodes a ServerHello handshake message body, excluding the four-byte handshake header.

func (*ServerHello) JA4S

func (sh *ServerHello) JA4S(quic bool) string

JA4S returns the JA4S server fingerprint, in the form "t130200_1301_a56c5b993250".

func (*ServerHello) NegotiatedVersion

func (sh *ServerHello) NegotiatedVersion() uint16

NegotiatedVersion returns the version actually in force.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL