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>
  • Loading branch information
arsenalzp committed Jul 3, 2024
1 parent dd39f97 commit a200310
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 85 deletions.
18 changes: 7 additions & 11 deletions pkg/bundle/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ 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 opts = util.CertPoolOptions{
FilterExpiredCerts: b.FilterExpiredCerts,
}
var certPool = util.NewCertPool(&opts)

for _, source := range bundle.Spec.Sources {
var (
Expand Down Expand Up @@ -99,27 +102,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
7 changes: 6 additions & 1 deletion pkg/fspkg/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ 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 opts = util.CertPoolOptions{
FilterExpiredCerts: false,
}
var certPool = util.NewCertPool(&opts)

err := util.ValidateAndSplitPEMBundle(certPool, []byte(p.Bundle))
if err != nil {
return fmt.Errorf("package bundle failed validation: %w", err)
}
Expand Down
51 changes: 42 additions & 9 deletions pkg/util/cert_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,35 @@ 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 CertPoolOptions struct {
FilterExpiredCerts bool
}

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

// 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 +82,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 +102,25 @@ func (cp *certPool) getCertsPEM() [][]byte {

return certsData
}

// Get certificates quantity in the certificates pool
func (cp *CertPool) getCertsQuantity() 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
}
10 changes: 8 additions & 2 deletions pkg/util/cert_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import (
)

func TestNewCertPool(t *testing.T) {
certPool := newCertPool(false)
var opts = CertPoolOptions{
FilterExpiredCerts: false,
}
certPool := NewCertPool(&opts)

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

// populate certificates bundle
for _, crt := range certificateList {
certPool := newCertPool(false)
var opts = CertPoolOptions{
FilterExpiredCerts: false,
}
certPool := NewCertPool(&opts)

if err := certPool.appendCertFromPEM([]byte(crt.certificate)); err != nil {
t.Fatalf("error adding PEM certificate into pool %s", err)
Expand Down
71 changes: 40 additions & 31 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,39 +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
return err
}

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

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

// ValidateAndSplitPEMBundleWithOptions takes a PEM bundle as input, validates it and
Expand All @@ -83,15 +64,13 @@ func ValidateAndSanitizePEMBundleWithOptions(data []byte, opts ValidateAndSaniti
// 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

func ValidateAndSplitPEMBundleWithOptions(certPool *CertPool, data []byte) error {
err := certPool.appendCertFromPEM(data)
if err != nil {
return nil, fmt.Errorf("invalid PEM block in bundle; invalid PEM certificate: %w", err)
return fmt.Errorf("invalid PEM block in bundle; invalid PEM certificate: %w", err)
}

return certPool.getCertsPEM(), nil
return nil
}

// DecodeX509CertificateChainBytes will decode a PEM encoded x509 Certificate chain.
Expand Down Expand Up @@ -121,3 +100,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.getCertsQuantity()
}
Loading

0 comments on commit a200310

Please sign in to comment.