package measurexlite // // TLS tracing // import ( "context" "crypto/tls" "crypto/x509" "errors" "net" "time" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/tracex" ) // NewTLSHandshakerStdlib is equivalent to netxlite.NewTLSHandshakerStdlib // except that it returns a model.TLSHandshaker that uses this trace. func (tx *Trace) NewTLSHandshakerStdlib(dl model.DebugLogger) model.TLSHandshaker { return &tlsHandshakerTrace{ thx: tx.newTLSHandshakerStdlib(dl), tx: tx, } } // tlsHandshakerTrace is a trace-aware TLS handshaker. type tlsHandshakerTrace struct { thx model.TLSHandshaker tx *Trace } var _ model.TLSHandshaker = &tlsHandshakerTrace{} // Handshake implements model.TLSHandshaker.Handshake. func (thx *tlsHandshakerTrace) Handshake( ctx context.Context, conn net.Conn, tlsConfig *tls.Config) (net.Conn, tls.ConnectionState, error) { return thx.thx.Handshake(netxlite.ContextWithTrace(ctx, thx.tx), conn, tlsConfig) } // OnTLSHandshakeStart implements model.Trace.OnTLSHandshakeStart. func (tx *Trace) OnTLSHandshakeStart(now time.Time, remoteAddr string, config *tls.Config) { t := now.Sub(tx.ZeroTime) select { case tx.networkEvent <- NewAnnotationArchivalNetworkEvent(tx.Index, t, "tls_handshake_start"): default: // buffer is full } } // OnTLSHandshakeDone implements model.Trace.OnTLSHandshakeDone. func (tx *Trace) OnTLSHandshakeDone(started time.Time, remoteAddr string, config *tls.Config, state tls.ConnectionState, err error, finished time.Time) { t := finished.Sub(tx.ZeroTime) select { case tx.tlsHandshake <- NewArchivalTLSOrQUICHandshakeResult( tx.Index, started.Sub(tx.ZeroTime), "tcp", remoteAddr, config, state, err, t, ): default: // buffer is full } select { case tx.networkEvent <- NewAnnotationArchivalNetworkEvent(tx.Index, t, "tls_handshake_done"): default: // buffer is full } } // NewArchivalTLSOrQUICHandshakeResult generates a model.ArchivalTLSOrQUICHandshakeResult // from the available information right after the TLS handshake returns. func NewArchivalTLSOrQUICHandshakeResult( index int64, started time.Duration, network string, address string, config *tls.Config, state tls.ConnectionState, err error, finished time.Duration) *model.ArchivalTLSOrQUICHandshakeResult { return &model.ArchivalTLSOrQUICHandshakeResult{ Network: network, Address: address, CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite), Failure: tracex.NewFailure(err), NegotiatedProtocol: state.NegotiatedProtocol, NoTLSVerify: config.InsecureSkipVerify, PeerCertificates: TLSPeerCerts(state, err), ServerName: config.ServerName, T0: started.Seconds(), T: finished.Seconds(), Tags: []string{}, TLSVersion: netxlite.TLSVersionString(state.Version), TransactionID: index, } } // newArchivalBinaryData is a factory that adapts binary data to the // model.ArchivalMaybeBinaryData format. func newArchivalBinaryData(data []byte) model.ArchivalMaybeBinaryData { // TODO(https://github.com/ooni/probe/issues/2165): we should actually extend the // model's archival data format to have a pure-binary-data type for the cases in which // we know in advance we're dealing with binary data. return model.ArchivalMaybeBinaryData{ Value: string(data), } } // TLSPeerCerts extracts the certificates either from the list of certificates // in the connection state or from the error that occurred. func TLSPeerCerts( state tls.ConnectionState, err error) (out []model.ArchivalMaybeBinaryData) { out = []model.ArchivalMaybeBinaryData{} var x509HostnameError x509.HostnameError if errors.As(err, &x509HostnameError) { // Test case: https://wrong.host.badssl.com/ out = append(out, newArchivalBinaryData(x509HostnameError.Certificate.Raw)) return } var x509UnknownAuthorityError x509.UnknownAuthorityError if errors.As(err, &x509UnknownAuthorityError) { // Test case: https://self-signed.badssl.com/. This error has // never been among the ones returned by MK. out = append(out, newArchivalBinaryData(x509UnknownAuthorityError.Cert.Raw)) return } var x509CertificateInvalidError x509.CertificateInvalidError if errors.As(err, &x509CertificateInvalidError) { // Test case: https://expired.badssl.com/ out = append(out, newArchivalBinaryData(x509CertificateInvalidError.Cert.Raw)) return } for _, cert := range state.PeerCertificates { out = append(out, newArchivalBinaryData(cert.Raw)) } return } // TLSHandshakes drains the network events buffered inside the TLSHandshake channel. func (tx *Trace) TLSHandshakes() (out []*model.ArchivalTLSOrQUICHandshakeResult) { for { select { case ev := <-tx.tlsHandshake: out = append(out, ev) default: return // done } } } // FirstTLSHandshakeOrNil drains the network events buffered inside the TLSHandshake channel // and returns the first TLSHandshake, if any. Otherwise, it returns nil. func (tx *Trace) FirstTLSHandshakeOrNil() *model.ArchivalTLSOrQUICHandshakeResult { ev := tx.TLSHandshakes() if len(ev) < 1 { return nil } return ev[0] }