Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for binary DER format cert files #974

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ validation checks and any behavior changes at that time noted.

| Flag | Required | Default | Repeat | Possible | Description |
| -------------------------------------------- | --------- | ------- | ------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `f`, `filename` | No | `false` | No | *valid file name characters* | Fully-qualified path to a PEM formatted certificate file containing one or more certificates. |
| `f`, `filename` | No | `false` | No | *valid file name characters* | Fully-qualified path to a PEM (text) or binary DER formatted certificate file containing one or more certificates. |
| `branding` | No | `false` | No | `branding` | Toggles emission of branding details with plugin status details. This output is disabled by default. |
| `h`, `help` | No | `false` | No | `h`, `help` | Show Help text along with the list of supported flags. |
| `v`, `verbose` | No | `false` | No | `v`, `verbose` | Toggles emission of detailed certificate metadata. This level of output is disabled by default. |
Expand Down Expand Up @@ -639,7 +639,7 @@ validation checks and any behavior changes at that time noted.

| Flag | Required | Default | Repeat | Possible | Description |
| -------------------- | --------- | ------- | ------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `f`, `filename` | No | `false` | No | *valid file name characters* | Fully-qualified path to a PEM formatted certificate file containing one or more certificates. |
| `f`, `filename` | No | `false` | No | *valid file name characters* | Fully-qualified path to a PEM (text) or binary DER formatted certificate file containing one or more certificates. |
| `text` | No | `false` | No | `true`, `false` | Toggles emission of x509 TLS certificates in an OpenSSL-inspired text format. This output is disabled by default. |
| `h`, `help` | No | `false` | No | `h`, `help` | Show Help text along with the list of supported flags. |
| `v`, `verbose` | No | `false` | No | `v`, `verbose` | Toggles emission of detailed certificate metadata. This level of output is disabled by default. |
Expand Down
258 changes: 235 additions & 23 deletions internal/certs/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package certs

import (
"bytes"
"crypto"

// We use this to verify MD5WithRSA signatures.
Expand Down Expand Up @@ -95,6 +96,34 @@ var (
// configuration validation requires that at least one validation check is
// performed.
ErrNoCertValidationResults = errors.New("certificate validation results collection is empty")

// ErrUnsupportedFileFormat indicates that parsing attempts against a
// given file have failed because the file is in an unsupported format.
ErrUnsupportedFileFormat = errors.New("unsupported file format")

// ErrEmptyCertificateFile indicates that decoding/parsing attempts have
// failed due to an empty input file.
ErrEmptyCertificateFile = errors.New("potentially empty certificate file")

// ErrPEMParseFailureMalformedCertificate indicates that PEM decoding
// attempts have failed due to the assumption that the given input
// certificate data is malformed.
ErrPEMParseFailureMalformedCertificate = errors.New("potentially malformed certificate")

// ErrPEMParseFailureEmptyCertificateBlock indicates that PEM decoding
// attempts have failed due to what appears to be an empty PEM certificate
// block in the given input.
//
// For example:
//
// -----BEGIN CERTIFICATE-----
// -----END CERTIFICATE-----
//
//
// See also:
//
// - https://github.com/smallstep/certinfo/pull/38
ErrPEMParseFailureEmptyCertificateBlock = errors.New("potentially empty certificate block")
)

// ServiceStater represents a type that is capable of evaluating its overall
Expand Down Expand Up @@ -179,6 +208,59 @@ type DiscoveredCertChain struct {
// specified hosts and ports.
type DiscoveredCertChains []DiscoveredCertChain

// PEM block type values (from preamble).
//
// See also:
//
// - https://pkg.go.dev/encoding/pem#Block
// - https://8gwifi.org/PemParserFunctions.jsp
// - https://stackoverflow.com/questions/5355046/where-is-the-pem-file-format-specified
// - https://github.com/openssl/openssl/blob/4f899849ceec7cd8e45da9aa1802df782cf80202/include/openssl/pem.h#L35
//
// #nosec G101 -- Ignore false positive matches
const (
PEMBlockTypeCRLBegin = "-----BEGIN X509 CRL-----"
PEMBlockTypeCRLEnd = "-----END X509 CRL-----"
PEMBlockTypeCRTBegin = "-----BEGIN CERTIFICATE-----"
PEMBlockTypeCRTEnd = "-----END CERTIFICATE-----"
PEMBlockTypeCSRBegin = "-----BEGIN CERTIFICATE REQUEST-----"
PEMBlockTypeCSREnd = "-----END CERTIFICATE REQUEST-----"
PEMBlockTypeNewCSRBegin = "-----BEGIN NEW CERTIFICATE REQUEST-----"
PEMBlockTypeNewCSREnd = "-----END NEW CERTIFICATE REQUEST-----"
PEMBlockTypePublicKeyBegin = "-----BEGIN RSA PUBLIC KEY-----"
PEMBlockTypePublicKeyEnd = "-----END RSA PUBLIC KEY-----"
PEMBlockTypeRSAPrivateKeyBegin = "-----BEGIN RSA PRIVATE KEY-----"
PEMBlockTypeRSAPrivateKeyEnd = "-----END RSA PRIVATE KEY-----"
PEMBlockTypeDSAPrivateKeyBegin = "-----BEGIN DSA PRIVATE KEY-----"
PEMBlockTypeDSAPrivateKeyEnd = "-----END DSA PRIVATE KEY-----"
PEMBlockTypeECPrivateKeyBegin = "-----BEGIN EC PRIVATE KEY-----"
PEMBlockTypeECPrivateKeyEnd = "-----END EC PRIVATE KEY-----"
PEMBlockTypePrivateKeyBegin = "-----BEGIN PRIVATE KEY-----"
PEMBlockTypePrivateKeyEnd = "-----END PRIVATE KEY-----"
PEMBlockTypePKCS7Begin = "-----BEGIN PKCS7-----"
PEMBlockTypePKCS7End = "-----END PKCS7-----"
PEMBlockTypePGPPrivateKeyBegin = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
PEMBlockTypePGPPrivateKeyEnd = "-----END PGP PRIVATE KEY BLOCK-----"
PEMBlockTypePGPPublicKeyBegin = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
PEMBlockTypePGPPublicKeyEnd = "-----END PGP PUBLIC KEY BLOCK-----"
)

// Human readable values for common PEM block types.
const (
PEMBlockTypeCRL = "certificate revocation list"
PEMBlockTypeCRT = "PEM encoded certificate"
PEMBlockTypeCSR = "certificate signing request"
PEMBlockTypeNewCSR = "certificate signing request"
PEMBlockTypePublicKey = "RSA public key"
PEMBlockTypeRSAPrivateKey = "RSA private key"
PEMBlockTypeDSAPrivateKey = "DSA private key"
PEMBlockTypeECPrivateKey = "EC private key"
PEMBlockTypePrivateKey = "private key"
PEMBlockTypePKCS7 = "PKCS7"
PEMBlockTypePGPPrivateKey = "PGP private key"
PEMBlockTypePGPPublicKey = "PGP public key"
)

// CertValidityDateLayout is the chosen date layout for displaying certificate
// validity date/time values across our application.
const CertValidityDateLayout string = "2006-01-02 15:04:05 -0700 MST"
Expand Down Expand Up @@ -289,38 +371,169 @@ func ServiceState(val ServiceStater) nagios.ServiceState {
}

// GetCertsFromFile is a helper function for retrieving a certificate chain
// from a specified PEM formatted certificate file. An error is returned if
// the file cannot be decoded and parsed (e.g., empty file, not PEM
// formatted). Any leading non-PEM formatted data is skipped while any
// trailing non-PEM formatted data is returned for potential further
// evaluation.
// from a specified certificate file. An error is returned if the file format
// cannot be decoded and parsed. Any trailing non-parsable data is returned
// for potential further evaluation.
func GetCertsFromFile(filename string) ([]*x509.Certificate, []byte, error) {

var certChain []*x509.Certificate

// Read in the entire PEM certificate file after first attempting to
// sanitize the input file variable contents.
pemData, err := os.ReadFile(filepath.Clean(filename))
// Anything from the specified file that couldn't be converted to a
// certificate chain. While likely not of high value by itself, failure to
// parse a certificate file indicates a likely source of trouble.
var parseAttemptLeftovers []byte

// Read in the entire certificate file after first attempting to sanitize
// the input file variable contents.
certFileData, err := os.ReadFile(filepath.Clean(filename))
if err != nil {
return nil, nil, err
}

// Grab the first PEM formatted block in our PEM cert file data.
block, rest := pem.Decode(pemData)
// Bail if nothing was found.
if len(certFileData) == 0 {
return nil, nil, fmt.Errorf(
"failed to decode %s as certificate file: %w",
filename,
ErrEmptyCertificateFile,
)
}

switch {
case block == nil:
// Do *NOT* normalize newlines on this content, strip blank lines only. If
// applied directly to DER encoded binary file content it will break
// parsing.
certFileData = textutils.StripBlankLines(certFileData)

unsupportedCertFormat := func(actualFormat string) ([]*x509.Certificate, []byte, error) {
return nil, nil, fmt.Errorf(
"failed to decode %s as PEM formatted certificate file; potentially malformed certificate",
"failed to decode %s (%s format) as certificate file: %w",
filename,
actualFormat,
ErrUnsupportedFileFormat,
)
case len(block.Bytes) == 0:
}

// Attempt to determine cert file type based on initial file contents. As
// of GH-862 only two input file formats are supported:
//
// - PEM (text) encoded ASN.1 DER
// - binary ASN.1 DER
//
// We attempt to match other known PEM encoded file formats and provide a
// useful error message to help sysadmins with troubleshooting.
switch {
case bytes.Contains(certFileData, []byte(PEMBlockTypeCRTBegin)):
// fmt.Println("File detected as PEM formatted")

// Attempt to parse as PEM encoded DER certificate file.
certChain, parseAttemptLeftovers, err = ParsePEMCertificates(certFileData)
if err != nil {
return nil, nil, fmt.Errorf(
"failed to decode %s as PEM formatted certificate file: %w",
filename,
err,
)
}

case bytes.Contains(certFileData, []byte(PEMBlockTypeCRLBegin)):
return unsupportedCertFormat(PEMBlockTypeCRL)

case bytes.Contains(certFileData, []byte(PEMBlockTypeCSRBegin)):
return unsupportedCertFormat(PEMBlockTypeCSR)

case bytes.Contains(certFileData, []byte(PEMBlockTypeNewCSRBegin)):
return unsupportedCertFormat(PEMBlockTypeNewCSR)

case bytes.Contains(certFileData, []byte(PEMBlockTypePublicKeyBegin)):
return unsupportedCertFormat(PEMBlockTypePublicKey)

case bytes.Contains(certFileData, []byte(PEMBlockTypeRSAPrivateKeyBegin)):
return unsupportedCertFormat(PEMBlockTypeRSAPrivateKey)

case bytes.Contains(certFileData, []byte(PEMBlockTypeDSAPrivateKeyBegin)):
return unsupportedCertFormat(PEMBlockTypeDSAPrivateKey)

case bytes.Contains(certFileData, []byte(PEMBlockTypeECPrivateKeyBegin)):
return unsupportedCertFormat(PEMBlockTypeECPrivateKey)

case bytes.Contains(certFileData, []byte(PEMBlockTypePrivateKeyBegin)):
return unsupportedCertFormat(PEMBlockTypePrivateKey)

case bytes.Contains(certFileData, []byte(PEMBlockTypePKCS7Begin)):
return unsupportedCertFormat(PEMBlockTypePKCS7)

case bytes.Contains(certFileData, []byte(PEMBlockTypePGPPrivateKeyBegin)):
return unsupportedCertFormat(PEMBlockTypePGPPrivateKey)

case bytes.Contains(certFileData, []byte(PEMBlockTypePGPPublicKeyBegin)):
return unsupportedCertFormat(PEMBlockTypePGPPublicKey)

default:
// Parse as ASN.1 (binary) DER data.
certChain, err = x509.ParseCertificates(certFileData)
if err != nil {
return nil, nil, fmt.Errorf(
"failed to decode %s as ASN.1 (binary) DER formatted certificate file: %w",
filename,
err,
)
}
}

return certChain, parseAttemptLeftovers, err

}

// GetCertsFromPEMFile is a helper function for retrieving a certificate chain
// from a specified PEM formatted certificate file. An error is returned if
// the file cannot be decoded and parsed (e.g., empty file, not PEM
// formatted). Any leading non-PEM formatted data is skipped while any
// trailing non-PEM formatted data is returned for potential further
// evaluation.
func GetCertsFromPEMFile(filename string) ([]*x509.Certificate, []byte, error) {
// Read in the entire certificate file after first attempting to sanitize
// the input file variable contents.
certFileData, err := os.ReadFile(filepath.Clean(filename))
if err != nil {
return nil, nil, err
}

certFileData = textutils.StripBlankLines(certFileData)

// Attempt to parse as PEM encoded DER certificate file.
certChain, parseAttemptLeftovers, err := ParsePEMCertificates(certFileData)
if err != nil {
return nil, nil, fmt.Errorf(
"failed to decode %s as PEM formatted certificate file; potentially empty certificate file",
"failed to decode %s as PEM formatted certificate file: %w",
filename,
err,
)
}

return certChain, parseAttemptLeftovers, nil
}

// ParsePEMCertificates retrieves the given byte slice as a PEM formatted
// certificate chain. Any leading non-PEM formatted data is skipped while any
// trailing non-PEM formatted data is returned for potential further
// evaluation. An error is returned if the given data cannot be decoded and
// parsed.
func ParsePEMCertificates(pemData []byte) ([]*x509.Certificate, []byte, error) {
var certChain []*x509.Certificate

// It's safe to normalize EOLs in PEM encoded data, but *not* in DER
// data itself.
pemData = textutils.NormalizeNewlines(pemData)

// Grab the first PEM formatted block.
block, parseAttemptLeftovers := pem.Decode(pemData)

switch {
case block == nil:
return nil, nil, ErrPEMParseFailureMalformedCertificate
case len(block.Bytes) == 0:
return nil, nil, ErrPEMParseFailureEmptyCertificateBlock
}

// If there is only one certificate (e.g., "server" or "leaf" certificate)
// we'll only get one block from the last pem.Decode() call. However, if
// the file contains a certificate chain or "bundle" we will need to call
Expand All @@ -331,7 +544,7 @@ func GetCertsFromFile(filename string) ([]*x509.Certificate, []byte, error) {

// fmt.Println("Type of block:", block.Type)
// fmt.Println("size of file content:", len(pemData))
// fmt.Println("size of rest:", len(rest))
// fmt.Println("size of parseAttemptLeftovers:", len(parseAttemptLeftovers))

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
Expand All @@ -341,10 +554,10 @@ func GetCertsFromFile(filename string) ([]*x509.Certificate, []byte, error) {
// we got a cert. Let's add it to our list
certChain = append(certChain, cert)

if len(rest) > 0 {
block, rest = pem.Decode(rest)
if len(parseAttemptLeftovers) > 0 {
block, parseAttemptLeftovers = pem.Decode(parseAttemptLeftovers)

// if we were able to decode the "rest" of the data, then
// if we were able to decode the rest of the data, then
// iterate again so we can parse it
if block != nil {
continue
Expand All @@ -356,13 +569,12 @@ func GetCertsFromFile(filename string) ([]*x509.Certificate, []byte, error) {

// we're done attempting to decode the cert file; we have found data
// that fails to decode properly
if len(rest) > 0 {
if len(parseAttemptLeftovers) > 0 {
break
}
}

return certChain, rest, err

return certChain, parseAttemptLeftovers, nil
}

// IsExpiredCert receives a x509 certificate and returns a boolean value
Expand Down
Loading
Loading