145 lines
4.7 KiB
Go
145 lines
4.7 KiB
Go
|
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),
|
||
|
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, address string, config *tls.Config,
|
||
|
state tls.ConnectionState, err error, finished time.Duration) *model.ArchivalTLSOrQUICHandshakeResult {
|
||
|
return &model.ArchivalTLSOrQUICHandshakeResult{
|
||
|
Address: address,
|
||
|
CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite),
|
||
|
Failure: tracex.NewFailure(err),
|
||
|
NegotiatedProtocol: state.NegotiatedProtocol,
|
||
|
NoTLSVerify: config.InsecureSkipVerify,
|
||
|
PeerCertificates: TLSPeerCerts(state, err),
|
||
|
ServerName: config.ServerName,
|
||
|
T: finished.Seconds(),
|
||
|
Tags: []string{},
|
||
|
TLSVersion: netxlite.TLSVersionString(state.Version),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
}
|
||
|
}
|