Skip to content

Commit

Permalink
Merge pull request #370 from erikgb/refactor-sync
Browse files Browse the repository at this point in the history
refactor: split bundle sync code into source and target
  • Loading branch information
cert-manager-prow[bot] authored Jun 10, 2024
2 parents adec444 + 96f9481 commit edf9529
Show file tree
Hide file tree
Showing 4 changed files with 911 additions and 848 deletions.
380 changes: 380 additions & 0 deletions pkg/bundle/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,380 @@
/*
Copyright 2021 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package bundle

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/pem"
"fmt"
"strings"

jks "github.com/pavlo-v-chernykh/keystore-go/v4"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"software.sslmate.com/src/go-pkcs12"

trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
"github.com/cert-manager/trust-manager/pkg/util"
)

const (
// DefaultJKSPassword is the default password that Java uses; it's a Java convention to use this exact password.
// Since we're not storing anything secret in the JKS files we generate, this password is not a meaningful security measure
// but seems often to be expected by applications consuming JKS files
DefaultJKSPassword = "changeit"
// DefaultPKCS12Password is the empty string, that will create a password-less PKCS12 truststore.
// Password-less PKCS is the new default Java truststore from Java 18.
// By password-less, it means the certificates are not encrypted, and it contains no MacData for integrity check.
DefaultPKCS12Password = ""
)

type notFoundError struct{ error }

// bundleData holds the result of a call to buildSourceBundle. It contains the resulting PEM-encoded
// certificate data from concatenating all the sources together, binary data for any additional formats and
// any metadata from the sources which needs to be exposed on the Bundle resource's status field.
type bundleData struct {
data string
binaryData map[string][]byte

defaultCAPackageStringID string
}

// buildSourceBundle retrieves and concatenates all source bundle data for this Bundle object.
// Each source data is validated and pruned to ensure that all certificates within are valid, and
// 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

for _, source := range bundle.Spec.Sources {
var (
sourceData string
err error
)

switch {
case source.ConfigMap != nil:
sourceData, err = b.configMapBundle(ctx, source.ConfigMap)

case source.Secret != nil:
sourceData, err = b.secretBundle(ctx, source.Secret)

case source.InLine != nil:
sourceData = *source.InLine

case source.UseDefaultCAs != nil:
if !*source.UseDefaultCAs {
continue
}

if b.defaultPackage == nil {
err = notFoundError{fmt.Errorf("no default package was specified when trust-manager was started; default CAs not available")}
} else {
sourceData = b.defaultPackage.Bundle
resolvedBundle.defaultCAPackageStringID = b.defaultPackage.StringID()
}
}

if err != nil {
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)
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 {
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 {
return bundleData{}, err
}

return resolvedBundle, nil
}

// configMapBundle returns the data in the source ConfigMap within the trust Namespace.
func (b *bundle) configMapBundle(ctx context.Context, ref *trustapi.SourceObjectKeySelector) (string, error) {
// this slice will contain a single ConfigMap if we fetch by name
// or potentially multiple ConfigMaps if we fetch by label selector
var configMaps []corev1.ConfigMap

// if Name is set, we `Get` by name
if ref.Name != "" {
cm := corev1.ConfigMap{}
if err := b.client.Get(ctx, client.ObjectKey{
Namespace: b.Namespace,
Name: ref.Name,
}, &cm); apierrors.IsNotFound(err) {
return "", notFoundError{err}
} else if err != nil {
return "", fmt.Errorf("failed to get ConfigMap %s/%s: %w", b.Namespace, ref.Name, err)
}

configMaps = []corev1.ConfigMap{cm}
} else {
// if Selector is set, we `List` by label selector
cml := corev1.ConfigMapList{}
selector, selectorErr := metav1.LabelSelectorAsSelector(ref.Selector)
if selectorErr != nil {
return "", fmt.Errorf("failed to parse label selector as Selector for ConfigMap in namespace %s: %w", b.Namespace, selectorErr)
}
if err := b.client.List(ctx, &cml, client.MatchingLabelsSelector{Selector: selector}); apierrors.IsNotFound(err) {
return "", notFoundError{err}
} else if err != nil {
return "", fmt.Errorf("failed to get ConfigMapList: %w", err)
}

configMaps = cml.Items
}

var results strings.Builder
for _, cm := range configMaps {
data, ok := cm.Data[ref.Key]
if !ok {
return "", notFoundError{fmt.Errorf("no data found in ConfigMap %s/%s at key %q", cm.Namespace, cm.Name, ref.Key)}
}
results.WriteString(data)
results.WriteByte('\n')
}
return results.String(), nil
}

// secretBundle returns the data in the source Secret within the trust Namespace.
func (b *bundle) secretBundle(ctx context.Context, ref *trustapi.SourceObjectKeySelector) (string, error) {
// this slice will contain a single Secret if we fetch by name
// or potentially multiple Secrets if we fetch by label selector
var secrets []corev1.Secret

// if Name is set, we `Get` by name
if ref.Name != "" {
s := corev1.Secret{}
if err := b.client.Get(ctx, client.ObjectKey{
Namespace: b.Namespace,
Name: ref.Name,
}, &s); apierrors.IsNotFound(err) {
return "", notFoundError{err}
} else if err != nil {
return "", fmt.Errorf("failed to get Secret %s/%s: %w", b.Namespace, ref.Name, err)
}

secrets = []corev1.Secret{s}
} else {
// if Selector is set, we `List` by label selector
sl := corev1.SecretList{}
selector, selectorErr := metav1.LabelSelectorAsSelector(ref.Selector)
if selectorErr != nil {
return "", fmt.Errorf("failed to parse label selector as Selector for Secret in namespace %s: %w", b.Namespace, selectorErr)
}
if err := b.client.List(ctx, &sl, client.MatchingLabelsSelector{Selector: selector}); apierrors.IsNotFound(err) {
return "", notFoundError{err}
} else if err != nil {
return "", fmt.Errorf("failed to get SecretList: %w", err)
}

secrets = sl.Items
}

var results strings.Builder
for _, secret := range secrets {
data, ok := secret.Data[ref.Key]
if !ok {
return "", notFoundError{fmt.Errorf("no data found in Secret %s/%s at key %q", secret.Namespace, secret.Name, ref.Key)}
}
results.Write(data)
results.WriteByte('\n')
}
return results.String(), nil
}

type jksEncoder struct {
password string
}

// encodeJKS creates a binary JKS file from the given PEM-encoded trust bundle and password.
// Note that the password is not treated securely; JKS files generally seem to expect a password
// to exist and so we have the option for one.
func (e jksEncoder) encode(trustBundle string) ([]byte, error) {
cas, err := util.DecodeX509CertificateChainBytes([]byte(trustBundle))
if err != nil {
return nil, fmt.Errorf("failed to decode trust bundle: %w", err)
}

// WithOrderedAliases ensures that trusted certs are added to the JKS file in order,
// which makes the files appear to be reliably deterministic.
ks := jks.New(jks.WithOrderedAliases())

for _, c := range cas {
alias := certAlias(c.Raw, c.Subject.String())

// Note on CreationTime:
// Debian's JKS trust store sets the creation time to match the time that certs are added to the
// trust store (i.e., it's effectively time.Now() at the instant the file is generated).
// Using that method would make our JKS files in trust-manager non-deterministic, leaving us with
// two options if we want to maintain determinism:
// - Using something from the cert being added (e.g. NotBefore / NotAfter)
// - Using a fixed time (i.e. unix epoch)
// We use NotBefore here, arbitrarily.

err = ks.SetTrustedCertificateEntry(alias, jks.TrustedCertificateEntry{
CreationTime: c.NotBefore,
Certificate: jks.Certificate{
Type: "X509",
Content: c.Raw,
},
})

if err != nil {
// this error should never happen if we set jks.Certificate correctly
return nil, fmt.Errorf("failed to add cert with alias %q to trust store: %w", alias, err)
}
}

buf := &bytes.Buffer{}

err = ks.Store(buf, []byte(e.password))
if err != nil {
return nil, fmt.Errorf("failed to create JKS file: %w", err)
}

return buf.Bytes(), nil
}

// certAlias creates a JKS-safe alias for the given DER-encoded certificate, such that
// any two certificates will have a different aliases unless they're identical in every way.
// This unique alias fixes an issue where we used the Issuer field as an alias, leading to
// different certs being treated as identical.
// The friendlyName is included in the alias as a UX feature when examining JKS files using a
// tool like `keytool`.
func certAlias(derData []byte, friendlyName string) string {
certHashBytes := sha256.Sum256(derData)
certHash := hex.EncodeToString(certHashBytes[:])

// Since certHash is the part which actually distinguishes between two
// certificates, put it first so that it won't be truncated if a cert
// with a really long subject is added. Not sure what the upper limit
// for length actually is, but it shouldn't matter here.

return certHash[:8] + "|" + friendlyName
}

type pkcs12Encoder struct {
password string
}

func (e pkcs12Encoder) encode(trustBundle string) ([]byte, error) {
cas, err := util.DecodeX509CertificateChainBytes([]byte(trustBundle))
if err != nil {
return nil, fmt.Errorf("failed to decode trust bundle: %w", err)
}

var entries []pkcs12.TrustStoreEntry
for _, c := range cas {
entries = append(entries, pkcs12.TrustStoreEntry{
Cert: c,
FriendlyName: certAlias(c.Raw, c.Subject.String()),
})
}

encoder := pkcs12.LegacyRC2

if e.password == "" {
encoder = pkcs12.Passwordless
}

return encoder.EncodeTrustStoreEntries(entries, e.password)
}

func (b *bundleData) populateData(bundles []string, target trustapi.BundleTarget) error {
b.data = strings.Join(bundles, "\n") + "\n"

if target.AdditionalFormats != nil {
b.binaryData = make(map[string][]byte)

if target.AdditionalFormats.JKS != nil {
encoded, err := jksEncoder{password: *target.AdditionalFormats.JKS.Password}.encode(b.data)
if err != nil {
return fmt.Errorf("failed to encode JKS: %w", err)
}
b.binaryData[target.AdditionalFormats.JKS.Key] = encoded
}

if target.AdditionalFormats.PKCS12 != nil {
encoded, err := pkcs12Encoder{password: *target.AdditionalFormats.PKCS12.Password}.encode(b.data)
if err != nil {
return fmt.Errorf("failed to encode PKCS12: %w", err)
}
b.binaryData[target.AdditionalFormats.PKCS12.Key] = encoded
}
}
return nil
}

// remove duplicate certificates from bundles
func deduplicateBundles(bundles []string) ([]string, error) {
var block *pem.Block

var certificatesHashes = make(map[[32]byte]struct{})
var dedupCerts []string

for _, cert := range bundles {
certBytes := []byte(cert)

LOOP:
for {
block, certBytes = pem.Decode(certBytes)
if block == nil {
break LOOP
}

if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("couldn't decode PEM block containing certificate")
}

// calculate hash sum of the given certificate
hash := sha256.Sum256(block.Bytes)
// check existence of the hash
if _, ok := certificatesHashes[hash]; !ok {
// neew to trim a newline which is added by Encoder
dedupCerts = append(dedupCerts, string(bytes.Trim(pem.EncodeToMemory(block), "\n")))
certificatesHashes[hash] = struct{}{}
}
}

}

return dedupCerts, nil
}
Loading

0 comments on commit edf9529

Please sign in to comment.