Skip to content

Commit

Permalink
Merge pull request #953 from atc0005/i862-add-support-for-der-encoded…
Browse files Browse the repository at this point in the history
…-cert-files

Add support for binary DER format cert files
  • Loading branch information
atc0005 authored Sep 27, 2024
2 parents 9851069 + 4a26654 commit c0f2a3a
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 25 deletions.
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

0 comments on commit c0f2a3a

Please sign in to comment.