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
- Variables
- func CipherByName(name string) (uint16, bool)
- func CipherName(id uint16) string
- func ExtensionName(id uint16) string
- func GroupByName(name string) (uint16, bool)
- func GroupName(id uint16) string
- func IsGREASE(v uint16) bool
- func ParseCertificateChain(body []byte) ([][]byte, bool)
- func ParseServerKeyExchangeGroup(body []byte) (group uint16, ok bool)
- func SigAlgName(id uint16) string
- func SigAlgPostQuantum(id uint16) bool
- func VersionName(v uint16) string
- type CipherProperties
- type ClientHello
- type ECHInfo
- type GroupProperties
- type HandshakeMessage
- type ServerHello
Constants ¶
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 ExtEarlyData uint16 = 42 ExtSupportedVersions uint16 = 43 ExtCookie uint16 = 44 ExtPSKKeyExchangeModes uint16 = 45 ExtCertificateAuthorities uint16 = 47 ExtPostHandshakeAuth uint16 = 49 ExtSignatureAlgorithmsCert uint16 = 50 ExtQUICTransportParams uint16 = 57 ExtNextProtocolNegotiation uint16 = 13172 ExtApplicationSettings uint16 = 17513 ExtApplicationSettingsOld uint16 = 17613 ExtEncryptedClientHello uint16 = 0xfe0d ExtRenegotiationInfo uint16 = 0xff01 )
Extension type codepoints.
const ( RecordChangeCipherSpec uint8 = 20 RecordAlert uint8 = 21 RecordHandshake uint8 = 22 RecordApplicationData uint8 = 23 )
TLS record content types (RFC 8446 section 5.1).
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).
const ( GroupSourceServerKeyExchange = "server_key_exchange" )
Key exchange sources, reported in ServerHello.GroupSource.
Variables ¶
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 ¶
CipherByName returns the codepoint for an IANA cipher suite name.
func CipherName ¶
CipherName returns the IANA name for a cipher suite codepoint, or its hex form when unrecognised.
func ExtensionName ¶
ExtensionName returns the registered name for an extension codepoint, or its hex form when unrecognised.
func GroupByName ¶
GroupByName returns the codepoint for a named group.
func IsGREASE ¶
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 ¶
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 ¶
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 ¶
SigAlgName returns the name for a signature-algorithm codepoint, or its hex form.
func SigAlgPostQuantum ¶
SigAlgPostQuantum reports whether a signature algorithm is post-quantum.
func VersionName ¶
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"`
// 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"`
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 ¶
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"`
// 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.