Documentation
¶
Overview ¶
Package inventory turns raw handshake observations into reportable records: named parameters, a post-quantum readiness verdict, and findings.
This is where policy lives. tlsparse deliberately says only what was on the wire; deciding that 3DES is worth flagging, or that a TLS 1.2 flow is noteworthy, is a judgement that changes over time and must not be welded into the parser.
Index ¶
Constants ¶
const ( FindingObsoleteProtocol = "obsolete_protocol" FindingLegacyProtocol = "legacy_protocol" FindingBrokenCipher = "broken_cipher" FindingWeakCipher = "weak_cipher" FindingNoForwardSecrecy = "no_forward_secrecy" FindingAnonymousKeyExch = "anonymous_key_exchange" FindingExportCipher = "export_cipher" FindingNullEncryption = "null_encryption" FindingSHA1Signature = "sha1_signature" FindingCompressionOffered = "compression_offered" FindingClassicalKeyExch = "classical_key_exchange" FindingECHOffered = "ech_offered" FindingWeakCertKey = "weak_certificate_key" FindingCertExpired = "certificate_expired" FindingCertSHA1 = "certificate_sha1_signature" FindingUnknownCipher = "unknown_cipher" )
Finding identifiers. Stable strings: reports, filters and any downstream tooling key off these, so they are part of the output contract.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Accumulator ¶
type Accumulator struct {
// contains filtered or unexported fields
}
Accumulator builds a Summary incrementally.
func NewAccumulator ¶
func NewAccumulator() *Accumulator
NewAccumulator returns an empty Accumulator.
func (*Accumulator) Add ¶
func (a *Accumulator) Add(r *Record)
Add folds one record into the summary.
func (*Accumulator) Aggregates ¶
func (a *Accumulator) Aggregates(topN int) []Aggregate
Aggregates returns the distinct findings, worst first. topN bounds the result; pass 0 for all.
func (*Accumulator) AggregatesDropped ¶
func (a *Accumulator) AggregatesDropped() int
AggregatesDropped is how many distinct findings were discarded because maxAggregates was reached. Non-zero means the inventory is incomplete.
func (*Accumulator) DistinctFindings ¶
func (a *Accumulator) DistinctFindings() int
DistinctFindings is the number of aggregates retained.
func (*Accumulator) Summary ¶
func (a *Accumulator) Summary(topN int) Summary
Summary finalises and returns the roll-up. topN bounds each distribution; pass 0 for no limit.
type Aggregate ¶
type Aggregate struct {
Transport string `json:"transport"`
ServerName string `json:"server_name,omitempty"`
ServerIP netip.Addr `json:"server_ip"`
ServerPort uint16 `json:"server_port"`
Version string `json:"version,omitempty"`
CipherSuite string `json:"cipher_suite,omitempty"`
Group string `json:"group,omitempty"`
ALPN string `json:"alpn,omitempty"`
// JA4s are the distinct client fingerprints seen for this finding,
// capped at maxTrackedClients.
JA4s []string `json:"ja4s,omitempty"`
PQ PQStatus `json:"pq_status"`
ECH bool `json:"ech,omitempty"`
// ECHConfigIDs is how many distinct config_ids were seen for this
// destination. More than one, across more than one connection, means
// the extension was GREASE: a real ECH config has a stable id.
ECHConfigIDs int `json:"ech_config_ids,omitempty"`
// ECHLikelyGREASE is that inference. When true the server name is a
// genuine destination despite the extension being present.
ECHLikelyGREASE bool `json:"ech_likely_grease,omitempty"`
ServerObserved bool `json:"server_observed"`
Count int `json:"count"`
// Clients is the number of distinct client addresses seen, capped at
// maxTrackedClients; ClientsCapped says the true number is higher.
Clients int `json:"clients"`
ClientsCapped bool `json:"clients_capped,omitempty"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
Severity Severity `json:"severity"`
Findings []string `json:"findings,omitempty"`
}
Aggregate is one distinct finding: a destination and the cryptography negotiated with it, however many times that happened.
type CertInfo ¶
type CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotAfter time.Time `json:"not_after"`
PublicKeyAlgorithm string `json:"public_key_algorithm"`
KeyBits int `json:"key_bits,omitempty"`
SignatureAlgorithm string `json:"signature_algorithm"`
SelfSigned bool `json:"self_signed,omitempty"`
}
CertInfo summarises one certificate. Available for TLS 1.2 and below only: TLS 1.3 encrypts the Certificate message, so on a modern handshake there is nothing to report and this stays empty.
type Finding ¶
type Finding struct {
ID string `json:"id"`
Severity Severity `json:"severity"`
Detail string `json:"detail"`
}
Finding is one thing worth reporting about a handshake.
type FindingCount ¶
type FindingCount struct {
ID string `json:"id"`
Severity Severity `json:"severity"`
Count int `json:"count"`
Example string `json:"example,omitempty"`
}
FindingCount aggregates one finding ID across the run.
type PQStatus ¶
type PQStatus string
PQStatus is the post-quantum readiness of a single handshake.
The distinction between the middle states is the point of the whole exercise. A client that lists X25519MLKEM768 in supported_groups but sends no key share for it will complete a fully classical handshake against any server that accepts the offer. Counting it as "post-quantum ready" is the most common way a migration dashboard flatters itself.
const ( // PQNegotiated: the server selected a post-quantum group. Actually done. PQNegotiated PQStatus = "post_quantum" // PQOffered: the client sent a post-quantum key share and the server // chose classical anyway. The client is ready; the server is not. PQOffered PQStatus = "offered_not_selected" // PQAdvertised: post-quantum appears only in supported_groups, with no // key share. Nothing post-quantum will happen without a retry. PQAdvertised PQStatus = "advertised_only" // PQClassical: no post-quantum group anywhere in the handshake. PQClassical PQStatus = "classical" // PQUnknown: not enough of the handshake was captured to say. PQUnknown PQStatus = "unknown" )
type Record ¶
type Record struct {
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
// Transport is "tcp" or "quic". Worth reporting because it changes
// what could have been seen: over QUIC the certificate is encrypted at
// the Handshake level, so an empty certificate list means "not
// visible", not "none presented".
Transport string `json:"transport"`
ClientIP netip.Addr `json:"client_ip"`
ClientPort uint16 `json:"client_port"`
ServerIP netip.Addr `json:"server_ip"`
ServerPort uint16 `json:"server_port"`
ServerName string `json:"server_name,omitempty"`
// ECH reports that an encrypted_client_hello extension was present.
//
// It does not mean ServerName is unreliable. Chrome sends a decoy ECH
// extension on connections where no ECH config exists — GREASE,
// deliberately shaped to be indistinguishable from the real thing so
// middleboxes cannot learn to reject it. A passive observer cannot tell
// the two apart from the extension alone; that is its design goal.
//
// In practice most of what carries this extension is GREASE and the
// server name is genuine, so treating presence as proof discards real
// hostnames on a false premise.
ECH bool `json:"ech,omitempty"`
// ECHConfigID is the config_id from the extension. Real ECH reuses the
// id published in DNS, so repeated connections to one host carry the
// same value; GREASE randomises it. Variation across connections is
// therefore evidence of GREASE — the only discriminator available
// without an active DNS lookup, and only in aggregate.
ECHConfigID uint8 `json:"ech_config_id,omitempty"`
VersionOffered string `json:"version_offered"`
Version string `json:"version,omitempty"`
CipherSuite string `json:"cipher_suite,omitempty"`
CipherSuitesOffered []string `json:"cipher_suites_offered"`
ForwardSecrecy *bool `json:"forward_secrecy,omitempty"`
Group string `json:"group,omitempty"`
GroupSource string `json:"group_source,omitempty"`
SupportedGroups []string `json:"supported_groups,omitempty"`
SignatureAlgorithms []string `json:"signature_algorithms,omitempty"`
ALPNOffered []string `json:"alpn_offered,omitempty"`
ALPN string `json:"alpn,omitempty"`
JA4 string `json:"ja4,omitempty"`
JA4S string `json:"ja4s,omitempty"`
PQ PQStatus `json:"pq_status"`
Findings []Finding `json:"findings,omitempty"`
Certificates []CertInfo `json:"certificates,omitempty"`
// ServerObserved is false when only the client side was captured. Such
// a record still reports what was offered, but nothing negotiated.
ServerObserved bool `json:"server_observed"`
Truncated bool `json:"truncated,omitempty"`
}
Record is one handshake, named and judged.
func (*Record) MaxSeverity ¶
MaxSeverity returns the highest severity among the findings.
type Summary ¶
type Summary struct {
Flows int `json:"flows"`
ServerObserved int `json:"server_observed"`
FirstSeen time.Time `json:"first_seen,omitempty"`
LastSeen time.Time `json:"last_seen,omitempty"`
Versions []Count `json:"versions"`
Ciphers []Count `json:"ciphers"`
Groups []Count `json:"groups"`
ALPN []Count `json:"alpn"`
Transports []Count `json:"transports"`
JA4 []Count `json:"ja4"`
ServerName []Count `json:"server_names"`
PQ []Count `json:"pq_status"`
// PQReadiness is the fraction of handshakes where a post-quantum group
// was actually negotiated, out of those where the negotiation was
// observed at all. Flows with no captured server response are excluded
// rather than counted as failures.
PQReadiness float64 `json:"pq_readiness"`
// ECHOffered counts handshakes carrying an encrypted_client_hello
// extension. Most are GREASE, so this is not a count of hidden
// destinations — see Record.ECH.
ECHOffered int `json:"ech_offered"`
// ECHLikelyGREASE counts destinations where a config_id that varies
// across connections shows the extension was a decoy.
ECHLikelyGREASE int `json:"ech_likely_grease"`
Findings []FindingCount `json:"findings"`
// DistinctFindings is how many aggregates the flows collapsed to. The
// ratio to Flows is a measure of how repetitive the traffic is.
DistinctFindings int `json:"distinct_findings"`
AggregatesDropped int `json:"aggregates_dropped,omitempty"`
}
Summary is the roll-up over every record.