diff --git a/go.mod b/go.mod index 6f824f6dd..e6ce58e7b 100644 --- a/go.mod +++ b/go.mod @@ -173,6 +173,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.35.0 go.opentelemetry.io/otel/sdk/metric v0.35.0 go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd + golang.org/x/crypto v0.5.0 golang.org/x/mod v0.6.0 golang.org/x/net v0.5.0 golang.org/x/oauth2 v0.3.0 @@ -425,7 +426,6 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.5.0 // indirect golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b golang.org/x/time v0.1.0 // indirect golang.org/x/tools v0.2.0 // indirect diff --git a/plugins/inputs/x509_cert/README.md b/plugins/inputs/x509_cert/README.md index de208800c..06c492b33 100644 --- a/plugins/inputs/x509_cert/README.md +++ b/plugins/inputs/x509_cert/README.md @@ -67,6 +67,9 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. - issuer_common_name - issuer_serial_number - san + - ocsp_stapled + - ocsp_status (when ocsp_stapled=yes) + - ocsp_verified (when ocsp_stapled=yes) - fields: - verification_code (int) - verification_error (string) @@ -74,12 +77,16 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. - age (int, seconds) - startdate (int, seconds) - enddate (int, seconds) + - ocsp_status_code (int) + - ocsp_next_update (int, seconds) + - ocsp_produced_at (int, seconds) + - ocsp_this_update (int, seconds) ## Example Output ```shell -x509_cert,common_name=ubuntu,source=/etc/ssl/certs/ssl-cert-snakeoil.pem,verification=valid age=7693222i,enddate=1871249033i,expiry=307666777i,startdate=1555889033i,verification_code=0i 1563582256000000000 -x509_cert,common_name=www.example.org,country=US,locality=Los\ Angeles,organization=Internet\ Corporation\ for\ Assigned\ Names\ and\ Numbers,organizational_unit=Technology,province=California,source=https://example.org:443,verification=invalid age=20219055i,enddate=1606910400i,expiry=43328144i,startdate=1543363200i,verification_code=1i,verification_error="x509: certificate signed by unknown authority" 1563582256000000000 -x509_cert,common_name=DigiCert\ SHA2\ Secure\ Server\ CA,country=US,organization=DigiCert\ Inc,source=https://example.org:443,verification=valid age=200838255i,enddate=1678276800i,expiry=114694544i,startdate=1362744000i,verification_code=0i 1563582256000000000 -x509_cert,common_name=DigiCert\ Global\ Root\ CA,country=US,organization=DigiCert\ Inc,organizational_unit=www.digicert.com,source=https://example.org:443,verification=valid age=400465455i,enddate=1952035200i,expiry=388452944i,startdate=1163116800i,verification_code=0i 1563582256000000000 +x509_cert,common_name=ubuntu,ocsp_stapled=no,source=/etc/ssl/certs/ssl-cert-snakeoil.pem,verification=valid age=7693222i,enddate=1871249033i,expiry=307666777i,startdate=1555889033i,verification_code=0i 1563582256000000000 +x509_cert,common_name=www.example.org,country=US,locality=Los\ Angeles,organization=Internet\ Corporation\ for\ Assigned\ Names\ and\ Numbers,organizational_unit=Technology,province=California,ocsp_stapled=no,source=https://example.org:443,verification=invalid age=20219055i,enddate=1606910400i,expiry=43328144i,startdate=1543363200i,verification_code=1i,verification_error="x509: certificate signed by unknown authority" 1563582256000000000 +x509_cert,common_name=DigiCert\ SHA2\ Secure\ Server\ CA,country=US,organization=DigiCert\ Inc,ocsp_stapled=no,source=https://example.org:443,verification=valid age=200838255i,enddate=1678276800i,expiry=114694544i,startdate=1362744000i,verification_code=0i 1563582256000000000 +x509_cert,common_name=DigiCert\ Global\ Root\ CA,country=US,organization=DigiCert\ Inc,organizational_unit=www.digicert.com,ocsp_stapled=yes,ocsp_status=good,ocsp_verified=yes,source=https://example.org:443,verification=valid age=400465455i,enddate=1952035200i,expiry=388452944i,ocsp_next_update=1676714398i,ocsp_produced_at=1676112480i,ocsp_status_code=0i,ocsp_this_update=1676109600i,startdate=1163116800i,verification_code=0i 1563582256000000000 ``` diff --git a/plugins/inputs/x509_cert/x509_cert.go b/plugins/inputs/x509_cert/x509_cert.go index cce786d26..e3687be9f 100644 --- a/plugins/inputs/x509_cert/x509_cert.go +++ b/plugins/inputs/x509_cert/x509_cert.go @@ -21,6 +21,7 @@ import ( "time" "github.com/pion/dtls/v2" + "golang.org/x/crypto/ocsp" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" @@ -93,7 +94,7 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { collectedUrls := append(c.locations, c.collectCertURLs()...) for _, location := range collectedUrls { - certs, err := c.getCert(location, time.Duration(c.Timeout)) + certs, ocspresp, err := c.getCert(location, time.Duration(c.Timeout)) if err != nil { acc.AddError(fmt.Errorf("cannot get SSL cert '%s': %s", location, err.Error())) } @@ -141,6 +142,55 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { fields["verification_code"] = 1 fields["verification_error"] = err.Error() } + // OCSPResponse only for leaf cert + if i == 0 && ocspresp != nil && len(*ocspresp) > 0 { + var ocspissuer *x509.Certificate + for _, chaincert := range certs[1:] { + if cert.Issuer.CommonName == chaincert.Subject.CommonName && + cert.Issuer.SerialNumber == chaincert.Subject.SerialNumber { + ocspissuer = chaincert + break + } + } + resp, err := ocsp.ParseResponse(*ocspresp, ocspissuer) + if err != nil { + if ocspissuer == nil { + tags["ocsp_stapled"] = "no" + fields["ocsp_error"] = err.Error() + } else { + ocspissuer = nil // retry parsing w/out issuer cert + resp, err = ocsp.ParseResponse(*ocspresp, ocspissuer) + } + } + if err != nil { + tags["ocsp_stapled"] = "no" + fields["ocsp_error"] = err.Error() + } else { + tags["ocsp_stapled"] = "yes" + if ocspissuer != nil { + tags["ocsp_verified"] = "yes" + } else { + tags["ocsp_verified"] = "no" + } + // resp.Status: 0=Good 1=Revoked 2=Unknown + fields["ocsp_status_code"] = resp.Status + switch resp.Status { + case 0: + tags["ocsp_status"] = "good" + case 1: + tags["ocsp_status"] = "revoked" + // Status=Good: revoked_at always = -62135596800 + fields["ocsp_revoked_at"] = resp.RevokedAt.Unix() + default: + tags["ocsp_status"] = "unknown" + } + fields["ocsp_produced_at"] = resp.ProducedAt.Unix() + fields["ocsp_this_update"] = resp.ThisUpdate.Unix() + fields["ocsp_next_update"] = resp.NextUpdate.Unix() + } + } else { + tags["ocsp_stapled"] = "no" + } acc.AddFields("x509_cert", fields, tags) if c.ExcludeRootCerts { @@ -186,13 +236,13 @@ func (c *X509Cert) serverName(u *url.URL) string { return u.Hostname() } -func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certificate, error) { +func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certificate, *[]byte, error) { protocol := u.Scheme switch u.Scheme { case "udp", "udp4", "udp6": ipConn, err := net.DialTimeout(u.Scheme, u.Host, timeout) if err != nil { - return nil, err + return nil, nil, err } defer ipConn.Close() @@ -204,7 +254,7 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica } conn, err := dtls.Client(ipConn, dtlsCfg) if err != nil { - return nil, err + return nil, nil, err } defer conn.Close() @@ -213,7 +263,7 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica for _, rawCert := range rawCerts { parsed, err := x509.ParseCertificate(rawCert) if err != nil { - return nil, err + return nil, nil, err } if parsed != nil { @@ -221,7 +271,7 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica } } - return certs, nil + return certs, nil, nil case "https": protocol = "tcp" if u.Port() == "" { @@ -231,11 +281,11 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica case "tcp", "tcp4", "tcp6": dialer, err := c.Proxy() if err != nil { - return nil, err + return nil, nil, err } ipConn, err := dialer.DialTimeout(protocol, u.Host, timeout) if err != nil { - return nil, err + return nil, nil, err } defer ipConn.Close() @@ -248,28 +298,29 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica hsErr := conn.Handshake() if hsErr != nil { - return nil, hsErr + return nil, nil, hsErr } certs := conn.ConnectionState().PeerCertificates + ocspresp := conn.ConnectionState().OCSPResponse - return certs, nil + return certs, &ocspresp, nil case "file": content, err := os.ReadFile(u.Path) if err != nil { - return nil, err + return nil, nil, err } var certs []*x509.Certificate for { block, rest := pem.Decode(bytes.TrimSpace(content)) if block == nil { - return nil, fmt.Errorf("failed to parse certificate PEM") + return nil, nil, fmt.Errorf("failed to parse certificate PEM") } if block.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, err + return nil, nil, err } certs = append(certs, cert) } @@ -278,11 +329,11 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica } content = rest } - return certs, nil + return certs, nil, nil case "smtp": ipConn, err := net.DialTimeout("tcp", u.Host, timeout) if err != nil { - return nil, err + return nil, nil, err } defer ipConn.Close() @@ -292,24 +343,24 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica smtpConn, err := smtp.NewClient(ipConn, u.Host) if err != nil { - return nil, err + return nil, nil, err } err = smtpConn.Hello(downloadTLSCfg.ServerName) if err != nil { - return nil, err + return nil, nil, err } id, err := smtpConn.Text.Cmd("STARTTLS") if err != nil { - return nil, err + return nil, nil, err } smtpConn.Text.StartResponse(id) defer smtpConn.Text.EndResponse(id) _, _, err = smtpConn.Text.ReadResponse(220) if err != nil { - return nil, fmt.Errorf("did not get 220 after STARTTLS: %s", err.Error()) + return nil, nil, fmt.Errorf("did not get 220 after STARTTLS: %s", err.Error()) } tlsConn := tls.Client(ipConn, downloadTLSCfg) @@ -317,14 +368,15 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica hsErr := tlsConn.Handshake() if hsErr != nil { - return nil, hsErr + return nil, nil, hsErr } certs := tlsConn.ConnectionState().PeerCertificates + ocspresp := tlsConn.ConnectionState().OCSPResponse - return certs, nil + return certs, &ocspresp, nil default: - return nil, fmt.Errorf("unsupported scheme '%s' in location %s", u.Scheme, u.String()) + return nil, nil, fmt.Errorf("unsupported scheme '%s' in location %s", u.Scheme, u.String()) } } diff --git a/plugins/inputs/x509_cert/x509_cert_test.go b/plugins/inputs/x509_cert/x509_cert_test.go index 4e3012323..80975f963 100644 --- a/plugins/inputs/x509_cert/x509_cert_test.go +++ b/plugins/inputs/x509_cert/x509_cert_test.go @@ -325,6 +325,7 @@ func TestGatherUDPCertIntegration(t *testing.T) { require.Len(t, acc.Errors, 0) require.True(t, acc.HasMeasurement("x509_cert")) + require.True(t, acc.HasTag("x509_cert", "ocsp_stapled")) } func TestGatherTCPCert(t *testing.T) { @@ -361,6 +362,7 @@ func TestGatherCertIntegration(t *testing.T) { require.NoError(t, m.Gather(&acc)) require.True(t, acc.HasMeasurement("x509_cert")) + require.True(t, acc.HasTag("x509_cert", "ocsp_stapled")) } func TestGatherCertMustNotTimeoutIntegration(t *testing.T) { @@ -379,6 +381,7 @@ func TestGatherCertMustNotTimeoutIntegration(t *testing.T) { require.NoError(t, m.Gather(&acc)) require.Empty(t, acc.Errors) require.True(t, acc.HasMeasurement("x509_cert")) + require.True(t, acc.HasTag("x509_cert", "ocsp_stapled")) } func TestSourcesToURLs(t *testing.T) {