From 65ae7b83d85d9d8c2e2451f8f8383568e09c765a Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Tue, 8 Nov 2022 00:03:06 +0100 Subject: [PATCH 1/7] Support for pfx certificates --- src/exporters/certHelpers.go | 64 +++++++++++++++++++++++------------- test/files/genCerts.sh | 3 ++ test/files/test.sh | 3 ++ 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/exporters/certHelpers.go b/src/exporters/certHelpers.go index d47acc6..6b090c8 100644 --- a/src/exporters/certHelpers.go +++ b/src/exporters/certHelpers.go @@ -8,6 +8,8 @@ import ( "encoding/base64" "encoding/pem" "io/ioutil" + + "golang.org/x/crypto/pkcs12" ) type certMetric struct { @@ -35,41 +37,59 @@ func secondsToExpiryFromCertAsBase64String(s string) ([]certMetric, error) { return secondsToExpiryFromCertAsBytes(certBytes) } +func decodeFromPKCS(certBytes []byte) ([]*pem.Block, error) { + var blocks []*pem.Block + pfx_blocks, err := pkcs12.ToPEM(certBytes, "") + if err != nil { + return nil, err + } + for _ , b := range pfx_blocks { + if b.Type == "CERTIFICATE" { + blocks = append(blocks, b) + } + } + return blocks, nil +} + + func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { var metrics []certMetric var blocks []*pem.Block // Export the first certificates in the certificate chain block, rest := pem.Decode(certBytes) - if block == nil { - return metrics, fmt.Errorf("Failed to parse as a pem") - } - blocks = append(blocks, block) - - // Export the remaining certificates in the certificate chain - for len(rest) != 0 { - block, rest = pem.Decode(rest) - if block == nil { - return metrics, fmt.Errorf("Failed to parse intermediate as a pem") + if block != nil { + blocks = append(blocks, block) + // Export the remaining certificates in the certificate chain + for len(rest) != 0 { + block, rest = pem.Decode(rest) + if block == nil { + return metrics, fmt.Errorf("Failed to parse intermediate as a pem") + } + if block.Type == "CERTIFICATE" { + blocks = append(blocks, block) + } } - if block.Type == "CERTIFICATE" { - blocks = append(blocks, block) + } else { + // Trype to parse as PKCS + pfx_blocks, err := decodeFromPKCS(certBytes) + if err != nil { + return metrics, fmt.Errorf("Failed to parse certificate") } + blocks = pfx_blocks } for _, block := range blocks { cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return metrics, err - } + if err == nil { + var metric certMetric + metric.notAfter = float64(cert.NotAfter.Unix()) + metric.durationUntilExpiry = time.Until(cert.NotAfter).Seconds() + metric.issuer = cert.Issuer.CommonName + metric.cn = cert.Subject.CommonName - var metric certMetric - metric.notAfter = float64(cert.NotAfter.Unix()) - metric.durationUntilExpiry = time.Until(cert.NotAfter).Seconds() - metric.issuer = cert.Issuer.CommonName - metric.cn = cert.Subject.CommonName - - metrics = append(metrics, metric) + metrics = append(metrics, metric) + } } return metrics, nil diff --git a/test/files/genCerts.sh b/test/files/genCerts.sh index cf77dbe..2724a0b 100755 --- a/test/files/genCerts.sh +++ b/test/files/genCerts.sh @@ -41,4 +41,7 @@ openssl x509 -req -in bundle_server.csr -CA bundle_root.cert -CAkey bundle_root. # create bundle cat bundle_server.cert bundle_root.cert > bundle.crt +# generate pfx +openssl pkcs12 -export -out bundle_pfx.crt -in bundle.crt -inkey bundle_root.key -passout pass: + popd \ No newline at end of file diff --git a/test/files/test.sh b/test/files/test.sh index 279e8a7..860b421 100755 --- a/test/files/test.sh +++ b/test/files/test.sh @@ -47,6 +47,7 @@ mkdir certs ./genCerts.sh certs $days >/dev/null 2>&1 ./genKubeConfig.sh certs ./ >/dev/null 2>&1 + # run exporter $CERT_EXPORTER_PATH -include-cert-glob=certs/*.crt -include-kubeconfig-glob=certs/kubeconfig & @@ -59,6 +60,8 @@ validateMetrics 'cert_exporter_cert_expires_in_seconds{cn="root",filename="certs validateMetrics 'cert_exporter_cert_expires_in_seconds{cn="example.com",filename="certs/server.crt",issuer="root",nodename="master0"}' $days validateMetrics 'cert_exporter_cert_expires_in_seconds{cn="bundle-root",filename="certs/bundle.crt",issuer="bundle-root",nodename="master0"}' $days validateMetrics 'cert_exporter_cert_expires_in_seconds{cn="example-bundle.be",filename="certs/bundle.crt",issuer="bundle-root",nodename="master0"}' $days +validateMetrics 'cert_exporter_cert_expires_in_seconds{cn="bundle-root",filename="certs/bundle_pfx.crt",issuer="bundle-root",nodename="master0"}' $days + validateMetrics 'cert_exporter_kubeconfig_expires_in_seconds{cn="root",filename="certs/kubeconfig",issuer="root",name="cluster1",nodename="master0",type="cluster"}' $days validateMetrics 'cert_exporter_kubeconfig_expires_in_seconds{cn="root",filename="certs/kubeconfig",issuer="root",name="cluster2",nodename="master0",type="cluster"}' $days From c8670432867e71dfbb809086bb9fa7fba985468a Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Tue, 8 Nov 2022 09:26:31 +0100 Subject: [PATCH 2/7] In case a block can't be parsed, keep reason as last error --- src/exporters/certHelpers.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/exporters/certHelpers.go b/src/exporters/certHelpers.go index 6b090c8..e1c1a46 100644 --- a/src/exporters/certHelpers.go +++ b/src/exporters/certHelpers.go @@ -55,6 +55,7 @@ func decodeFromPKCS(certBytes []byte) ([]*pem.Block, error) { func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { var metrics []certMetric var blocks []*pem.Block + var last_err error // Export the first certificates in the certificate chain block, rest := pem.Decode(certBytes) @@ -89,8 +90,14 @@ func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { metric.cn = cert.Subject.CommonName metrics = append(metrics, metric) + } else { + last_err = err } } + if len(metrics) == 0 { + return metrics, last_err + } + return metrics, nil } From 7697528f35ec6d3764c87d8c523e38bf71be71f1 Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Tue, 8 Nov 2022 10:36:18 +0100 Subject: [PATCH 3/7] Update go modules and fix typo --- go.mod | 10 +++++----- go.sum | 10 ++++++++++ src/exporters/certHelpers.go | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 3e15f07..8644e0d 100644 --- a/go.mod +++ b/go.mod @@ -32,12 +32,12 @@ require ( github.com/prometheus/common v0.15.0 // indirect github.com/prometheus/procfs v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f // indirect - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/net v0.1.0 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e // indirect - golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 // indirect - golang.org/x/text v0.3.4 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/term v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/protobuf v1.25.0 // indirect diff --git a/go.sum b/go.sum index 633d55d..73459d7 100644 --- a/go.sum +++ b/go.sum @@ -411,6 +411,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -472,6 +474,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -524,8 +528,12 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -533,6 +541,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/src/exporters/certHelpers.go b/src/exporters/certHelpers.go index e1c1a46..80d4e1a 100644 --- a/src/exporters/certHelpers.go +++ b/src/exporters/certHelpers.go @@ -72,7 +72,7 @@ func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { } } } else { - // Trype to parse as PKCS + // Parse as PKCS ? pfx_blocks, err := decodeFromPKCS(certBytes) if err != nil { return metrics, fmt.Errorf("Failed to parse certificate") From 048a1ded1b1faa89d221f8a808e6d1144ce2852b Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Sun, 13 Nov 2022 15:10:14 +0100 Subject: [PATCH 4/7] indication that there was an attempt to parse as both pem and pkcs12 --- src/exporters/certHelpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exporters/certHelpers.go b/src/exporters/certHelpers.go index 80d4e1a..a66f0d2 100644 --- a/src/exporters/certHelpers.go +++ b/src/exporters/certHelpers.go @@ -75,7 +75,7 @@ func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { // Parse as PKCS ? pfx_blocks, err := decodeFromPKCS(certBytes) if err != nil { - return metrics, fmt.Errorf("Failed to parse certificate") + return metrics, fmt.Errorf("failed to parse as pem and pkcs12") } blocks = pfx_blocks } From cfa39d7f1b9c132970cca249ad1c8f35b08b19ce Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Mon, 14 Nov 2022 15:33:19 +0100 Subject: [PATCH 5/7] Handle PEM and PKCS in different methods --- src/exporters/certHelpers.go | 101 +++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/src/exporters/certHelpers.go b/src/exporters/certHelpers.go index a66f0d2..e83a386 100644 --- a/src/exporters/certHelpers.go +++ b/src/exporters/certHelpers.go @@ -37,67 +37,86 @@ func secondsToExpiryFromCertAsBase64String(s string) ([]certMetric, error) { return secondsToExpiryFromCertAsBytes(certBytes) } -func decodeFromPKCS(certBytes []byte) ([]*pem.Block, error) { +func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { + var metrics []certMetric + + parsed, metrics, err := parseAsPEM(certBytes) + if parsed { + return metrics, err + } else { + // Parse as PKCS ? + parsed, metrics, err := parseAsPKCS(certBytes) + if parsed { + return metrics, nil + } + return nil, fmt.Errorf("failed to parse as pem and pkcs12: %w", err) + } +} + +func getCertificateMetrics(cert *x509.Certificate)(certMetric) { + var metric certMetric + metric.notAfter = float64(cert.NotAfter.Unix()) + metric.durationUntilExpiry = time.Until(cert.NotAfter).Seconds() + metric.issuer = cert.Issuer.CommonName + metric.cn = cert.Subject.CommonName + return metric +} + +func parseAsPKCS(certBytes []byte) (bool, []certMetric, error) { + var metrics []certMetric var blocks []*pem.Block + var last_err error + pfx_blocks, err := pkcs12.ToPEM(certBytes, "") if err != nil { - return nil, err + return false, nil, err } for _ , b := range pfx_blocks { if b.Type == "CERTIFICATE" { blocks = append(blocks, b) } } - return blocks, nil -} + for _, block := range blocks { + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + var metric = getCertificateMetrics(cert) + metrics = append(metrics, metric) + } else { + last_err = err + } + } + return true, metrics, last_err +} -func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { +func parseAsPEM(certBytes []byte)(bool, []certMetric, error) { var metrics []certMetric var blocks []*pem.Block - var last_err error - - // Export the first certificates in the certificate chain + block, rest := pem.Decode(certBytes) - if block != nil { - blocks = append(blocks, block) - // Export the remaining certificates in the certificate chain - for len(rest) != 0 { - block, rest = pem.Decode(rest) - if block == nil { - return metrics, fmt.Errorf("Failed to parse intermediate as a pem") - } - if block.Type == "CERTIFICATE" { - blocks = append(blocks, block) - } + if block == nil { + return false, metrics, fmt.Errorf("Failed to parse as a pem") + } + blocks = append(blocks, block) + // Export the remaining certificates in the certificate chain + for len(rest) != 0 { + block, rest = pem.Decode(rest) + if block == nil { + return true, metrics, fmt.Errorf("Failed to parse intermediate as a pem") } - } else { - // Parse as PKCS ? - pfx_blocks, err := decodeFromPKCS(certBytes) - if err != nil { - return metrics, fmt.Errorf("failed to parse as pem and pkcs12") + if block.Type == "CERTIFICATE" { + blocks = append(blocks, block) } - blocks = pfx_blocks } - for _, block := range blocks { cert, err := x509.ParseCertificate(block.Bytes) - if err == nil { - var metric certMetric - metric.notAfter = float64(cert.NotAfter.Unix()) - metric.durationUntilExpiry = time.Until(cert.NotAfter).Seconds() - metric.issuer = cert.Issuer.CommonName - metric.cn = cert.Subject.CommonName - - metrics = append(metrics, metric) + if err != nil { + return true, metrics ,err } else { - last_err = err + var metric = getCertificateMetrics(cert) + metrics = append(metrics, metric) } } - - if len(metrics) == 0 { - return metrics, last_err - } - - return metrics, nil + return true, metrics, nil } + From 9793fab7a7cba9b70613d378aa2960825a1d2837 Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Wed, 16 Nov 2022 20:12:49 +0100 Subject: [PATCH 6/7] Short refactor after review for conditional statements --- src/exporters/certHelpers.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/exporters/certHelpers.go b/src/exporters/certHelpers.go index e83a386..0b830c0 100644 --- a/src/exporters/certHelpers.go +++ b/src/exporters/certHelpers.go @@ -43,14 +43,13 @@ func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) { parsed, metrics, err := parseAsPEM(certBytes) if parsed { return metrics, err - } else { - // Parse as PKCS ? - parsed, metrics, err := parseAsPKCS(certBytes) - if parsed { - return metrics, nil - } - return nil, fmt.Errorf("failed to parse as pem and pkcs12: %w", err) } + // Parse as PKCS ? + parsed, metrics, err = parseAsPKCS(certBytes) + if parsed { + return metrics, nil + } + return nil, fmt.Errorf("failed to parse as pem and pkcs12: %w", err) } func getCertificateMetrics(cert *x509.Certificate)(certMetric) { @@ -112,10 +111,9 @@ func parseAsPEM(certBytes []byte)(bool, []certMetric, error) { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return true, metrics ,err - } else { - var metric = getCertificateMetrics(cert) - metrics = append(metrics, metric) } + var metric = getCertificateMetrics(cert) + metrics = append(metrics, metric) } return true, metrics, nil } From ef1c7a7b7ad932d725e650988ef96d9d73777a40 Mon Sep 17 00:00:00 2001 From: Enrique Sanjuanelo Date: Wed, 16 Nov 2022 20:16:59 +0100 Subject: [PATCH 7/7] Update readme with pkcs12 format support --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 8fffa2b..44626d1 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ Kubernetes uses PKI certificates for authentication between all major components cert-exporter can publish metrics about -- x509 certificates on disk encoded in the [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) +- x509 certificates on disk encoded in the [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) and [PKCS12 format](https://en.wikipedia.org/wiki/PKCS_12) - Certs embedded or referenced from kubeconfig files. - Certs stored in Kubernetes - secrets