Skip to content

Commit

Permalink
introduce certitiface pool sctructure
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add a test for duplicate certs

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add comment for functions

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

fix typos, add comments for PEM and Certificates pool functions

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

improve functions naming in PEM logic

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

improve PEM functions

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

remove unused functions, add Option pattern

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

align code with the project style

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>
  • Loading branch information
arsenalzp committed Jul 8, 2024
1 parent dd39f97 commit e0e7df7
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 96 deletions.
15 changes: 4 additions & 11 deletions pkg/bundle/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type bundleData struct {
// is each bundle is concatenated together with a new line character.
func (b *bundle) buildSourceBundle(ctx context.Context, bundle *trustapi.Bundle) (bundleData, error) {
var resolvedBundle bundleData
var bundles []string
var certPool = util.NewCertPool(util.WithFilteredExpiredCerts(b.FilterExpiredCerts))

for _, source := range bundle.Spec.Sources {
var (
Expand Down Expand Up @@ -99,27 +99,20 @@ func (b *bundle) buildSourceBundle(ctx context.Context, bundle *trustapi.Bundle)
return bundleData{}, fmt.Errorf("failed to retrieve bundle from source: %w", err)
}

opts := util.ValidateAndSanitizeOptions{FilterExpired: b.Options.FilterExpiredCerts}
sanitizedBundle, err := util.ValidateAndSanitizePEMBundleWithOptions([]byte(sourceData), opts)
err = util.ValidateAndSplitPEMBundle(certPool, []byte(sourceData))
if err != nil {
return bundleData{}, fmt.Errorf("invalid PEM data in source: %w", err)
}

bundles = append(bundles, string(sanitizedBundle))
}

// NB: empty bundles are not valid so check and return an error if one somehow snuck through.

if len(bundles) == 0 {
if util.GetCertificatesQuantity(certPool) == 0 {
return bundleData{}, fmt.Errorf("couldn't find any valid certificates in bundle")
}

deduplicatedBundles, err := deduplicateBundles(bundles)
if err != nil {
return bundleData{}, err
}

if err := resolvedBundle.populateData(deduplicatedBundles, bundle.Spec.Target); err != nil {
if err := resolvedBundle.populateData(util.AsPEMBundleStrings(certPool), bundle.Spec.Target); err != nil {
return bundleData{}, err
}

Expand Down
5 changes: 4 additions & 1 deletion pkg/fspkg/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ func (p *Package) Clone() *Package {
func (p *Package) Validate() error {
// Ignore the sanitized bundle here and preserve the bundle as-is.
// We'll sanitize later, when building a bundle on a reconcile.
_, err := util.ValidateAndSanitizePEMBundle([]byte(p.Bundle))

var certPool = util.NewCertPool(util.WithFilteredExpiredCerts(false))

err := util.ValidateAndSplitPEMBundle(certPool, []byte(p.Bundle))
if err != nil {
return fmt.Errorf("package bundle failed validation: %w", err)
}
Expand Down
60 changes: 51 additions & 9 deletions pkg/util/cert_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,44 @@ limitations under the License.
package util

import (
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"time"
)

// CertPool is a set of certificates.
type certPool struct {
certificates []*x509.Certificate
filterExpired bool
type CertPool struct {
certificatesHashes map[[32]byte]struct{}
certificates []*x509.Certificate
filterExpired bool
}

type Option func(*CertPool)

func WithFilteredExpiredCerts(filterExpired bool) Option {
return func(cp *CertPool) {
cp.filterExpired = filterExpired
}
}

// newCertPool returns a new, empty CertPool.
func newCertPool(filterExpired bool) *certPool {
return &certPool{
certificates: make([]*x509.Certificate, 0),
filterExpired: filterExpired,
func NewCertPool(options ...Option) *CertPool {
var certPool = &CertPool{
certificates: make([]*x509.Certificate, 0),
certificatesHashes: make(map[[32]byte]struct{}),
}

for _, option := range options {
option(certPool)
}

return certPool
}

// Append certificate to a pool
func (cp *certPool) appendCertFromPEM(pemData []byte) error {
func (cp *CertPool) appendCertFromPEM(pemData []byte) error {
if pemData == nil {
return fmt.Errorf("certificate data can't be nil")
}
Expand Down Expand Up @@ -75,14 +91,18 @@ func (cp *certPool) appendCertFromPEM(pemData []byte) error {
continue
}

if cp.isDuplicate(certificate) {
continue
}

cp.certificates = append(cp.certificates, certificate)
}

return nil
}

// Get PEM certificates from pool
func (cp *certPool) getCertsPEM() [][]byte {
func (cp *CertPool) getCertsPEM() [][]byte {
var certsData [][]byte = make([][]byte, len(cp.certificates))

for i, cert := range cp.certificates {
Expand All @@ -91,3 +111,25 @@ func (cp *certPool) getCertsPEM() [][]byte {

return certsData
}

// Get certificates quantity in the certificates pool
func (cp *CertPool) size() int {
return len(cp.certificates)
}

// Check deplicates of certificate in the certificates pool
func (cp *CertPool) isDuplicate(cert *x509.Certificate) bool {
hash := sha256.Sum256(cert.Raw)
// check existence of the hash
if _, ok := cp.certificatesHashes[hash]; !ok {
cp.certificatesHashes[hash] = struct{}{}
return false
}

return true
}

// Get the full list of x509 Certificates from the certificates pool
func (cp *CertPool) getCertsList() []*x509.Certificate {
return cp.certificates
}
4 changes: 2 additions & 2 deletions pkg/util/cert_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
)

func TestNewCertPool(t *testing.T) {
certPool := newCertPool(false)
certPool := NewCertPool(WithFilteredExpiredCerts(false))

if certPool == nil {
t.Fatal("pool is nil")
Expand Down Expand Up @@ -76,7 +76,7 @@ func TestAppendCertFromPEM(t *testing.T) {

// populate certificates bundle
for _, crt := range certificateList {
certPool := newCertPool(false)
certPool := NewCertPool(WithFilteredExpiredCerts(false))

if err := certPool.appendCertFromPEM([]byte(crt.certificate)); err != nil {
t.Fatalf("error adding PEM certificate into pool %s", err)
Expand Down
80 changes: 37 additions & 43 deletions pkg/util/pem.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
)

// ValidateAndSanitizePEMBundle strictly validates a given input PEM bundle to confirm it contains
Expand All @@ -42,56 +43,19 @@ import (
// contain (accidental) private information. They're also non-standard according to
// https://www.rfc-editor.org/rfc/rfc7468

type ValidateAndSanitizeOptions struct {
FilterExpired bool // If true, expired certificates will be filtered out
}

// ValidateAndSanitizePEMBundle keeps the original function signature for backward compatibility
func ValidateAndSanitizePEMBundle(data []byte) ([]byte, error) {
opts := ValidateAndSanitizeOptions{
FilterExpired: false,
}
return ValidateAndSanitizePEMBundleWithOptions(data, opts)
}

// ValidateAndSplitPEMBundle keeps the original function signature for backward compatibility
func ValidateAndSplitPEMBundle(data []byte) ([][]byte, error) {
opts := ValidateAndSanitizeOptions{
FilterExpired: false,
}
return ValidateAndSplitPEMBundleWithOptions(data, opts)
}

// See also https://github.com/golang/go/blob/5d5ed57b134b7a02259ff070864f753c9e601a18/src/crypto/x509/cert_pool.go#L201-L239
// An option to enable filtering of expired certificates is available.
func ValidateAndSanitizePEMBundleWithOptions(data []byte, opts ValidateAndSanitizeOptions) ([]byte, error) {
certificates, err := ValidateAndSplitPEMBundleWithOptions(data, opts)
func ValidateAndSplitPEMBundle(certPool *CertPool, data []byte) error {
err := certPool.appendCertFromPEM(data)
if err != nil {
return nil, err
}

if len(certificates) == 0 {
return nil, fmt.Errorf("bundle contains no PEM certificates")
return err
}

return bytes.TrimSpace(bytes.Join(certificates, nil)), nil
}

// ValidateAndSplitPEMBundleWithOptions takes a PEM bundle as input, validates it and
// returns the list of certificates as a slice, allowing them to be iterated over.
// This process involves performs deduplication of certificates to ensure
// no duplicated certificates in the bundle.
// For details of the validation performed, see the comment for ValidateAndSanitizePEMBundle
// An option to enable filtering of expired certificates is available.
func ValidateAndSplitPEMBundleWithOptions(data []byte, opts ValidateAndSanitizeOptions) ([][]byte, error) {
var certPool *certPool = newCertPool(opts.FilterExpired) // put PEM encoded certificate into a pool

err := certPool.appendCertFromPEM(data)
if err != nil {
return nil, fmt.Errorf("invalid PEM block in bundle; invalid PEM certificate: %w", err)
if certPool.size() == 0 {
return fmt.Errorf("bundle contains no PEM certificates")
}

return certPool.getCertsPEM(), nil
return nil
}

// DecodeX509CertificateChainBytes will decode a PEM encoded x509 Certificate chain.
Expand Down Expand Up @@ -121,3 +85,33 @@ func DecodeX509CertificateChainBytes(certBytes []byte) ([]*x509.Certificate, err

return certs, nil
}

// Get the split bundle of all certificates in the certificates pool as representation of [][]byte
func AsSplitPEMBundle(certPool *CertPool) [][]byte {
return certPool.getCertsPEM()
}

// Get the split bundle of all certificates in the certificates pool as representation of []byte
func AsPEMBundleBytes(certPool *CertPool) []byte {
return bytes.TrimSpace(bytes.Join(certPool.getCertsPEM(), nil))
}

// Get the split bundle of all certificates in the certificates pool as representation of []string
func AsPEMBundleStrings(certPool *CertPool) []string {
var certList = make([]string, 0)

for _, cert := range certPool.getCertsPEM() {
certList = append(certList, strings.TrimSpace(string(cert)))
}

return certList
}

// Get the list of all x509 Certificates in the certificates pool
func AsCertificateList(certPool *CertPool) []*x509.Certificate {
return certPool.getCertsList()
}

func GetCertificatesQuantity(certPool *CertPool) int {
return certPool.size()
}
Loading

0 comments on commit e0e7df7

Please sign in to comment.