feat(inputs.x509_cert): Add OCSP stapling information for leaf certificates (#10550) (#12444)

Co-authored-by: Josh Powers <powersj@fastmail.com>
This commit is contained in:
Jarno Huuskonen 2023-02-17 10:47:54 +02:00 committed by GitHub
parent 0fd618ef84
commit 54c091977c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 27 deletions

2
go.mod
View File

@ -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

View File

@ -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
```

View File

@ -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())
}
}

View File

@ -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) {