Skip to content
Draft
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
6 changes: 4 additions & 2 deletions internal/certificate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
)

func GenerateLeaf(
certParser *pki.CertParser,
leafDNSNames []string,
leafDuration time.Duration,
caCert *x509.Certificate, caPk crypto.PrivateKey,
Expand Down Expand Up @@ -61,11 +62,12 @@ func GenerateLeaf(
}

// Sign certificate using CA
cert, err := pki.SignCertificate(template, caCert, pk.Public(), caPk)
cert, err := pki.SignCertificate(certParser, template, caCert, pk.Public(), caPk)
return cert, pk, err
}

func GenerateCA(
certParser *pki.CertParser,
caDuration time.Duration,
) (*x509.Certificate, crypto.Signer, error) {
pk, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
Expand Down Expand Up @@ -94,6 +96,6 @@ func GenerateCA(
}

// self sign the root CA
cert, err := pki.SignCertificate(template, template, pk.Public(), pk)
cert, err := pki.SignCertificate(certParser, template, template, pk.Public(), pk)
return cert, pk, err
}
38 changes: 36 additions & 2 deletions internal/pki/cert_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,45 @@ limitations under the License.
package pki

import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"slices"
)

type CertParser struct {
parsedCerts []parsedCert
}

func NewCertParser() *CertParser {
return &CertParser{}
}

type parsedCert struct {
certBytes []byte
certificate *x509.Certificate
err error
}

func (cp *CertParser) parseCertificateDER(certBytes []byte) (*x509.Certificate, error) {
i, found := slices.BinarySearchFunc(cp.parsedCerts, certBytes, func(a parsedCert, b []byte) int {
return bytes.Compare(a.certBytes, b)
})
if found {
parsedCert := cp.parsedCerts[i]
return parsedCert.certificate, parsedCert.err
}

certificate, err := x509.ParseCertificate(certBytes)
cp.parsedCerts = slices.Insert(cp.parsedCerts, i, parsedCert{
certBytes: certBytes,
certificate: certificate,
err: err,
})
return certificate, err
}

// parseCertificatePEM strictly validates a given input PEM bundle to confirm it contains
// only valid CERTIFICATE PEM blocks. If successful, returns nil and invokes addCert for
// each parsed certificate. Any comments or extra non-whitespace data cause an error.
Expand All @@ -32,7 +66,7 @@ import (
// If the callback returns false and there remains unread non-whitespace input,
// the function returns an error about extra data; if no extra data remains, the
// function returns nil.
func parseCertificatePEM(pemData []byte, addCert func(*x509.Certificate) (bool, error)) error {
func (cp *CertParser) parseCertificatePEM(pemData []byte, addCert func(*x509.Certificate) (bool, error)) error {
if pemData == nil {
return fmt.Errorf("certificate data can't be nil")
}
Expand All @@ -59,7 +93,7 @@ func parseCertificatePEM(pemData []byte, addCert func(*x509.Certificate) (bool,
return fmt.Errorf("invalid PEM block in bundle: PEM headers are not permitted")
}

certificate, err := x509.ParseCertificate(block.Bytes)
certificate, err := cp.parseCertificateDER(block.Bytes)
if err != nil {
return fmt.Errorf("invalid PEM block in bundle: failed to parse certificate: %w", err)
}
Expand Down
24 changes: 22 additions & 2 deletions internal/pki/cert_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/base32"
"encoding/pem"
"fmt"
"maps"
"slices"
"strings"
"time"
)

Expand Down Expand Up @@ -69,9 +71,9 @@ func (cp *CertPool) addCert(now time.Time, cert *x509.Certificate) error {
return nil
}

func (cp *CertPool) AddCertificatesFromPEM(pemData []byte) error {
func (cp *CertPool) AddCertificatesFromPEM(parser *CertParser, pemData []byte) error {
now := time.Now()
return parseCertificatePEM(pemData, func(cert *x509.Certificate) (bool, error) {
return parser.parseCertificatePEM(pemData, func(cert *x509.Certificate) (bool, error) {
if err := cp.addCert(now, cert); err != nil {
return false, err
}
Expand Down Expand Up @@ -115,3 +117,21 @@ func (cp *CertPool) Certificates() []*x509.Certificate {
}
return orderedCertificates
}

func (cp *CertPool) HashString() string {
return HashString(CertificatesHash(cp.Certificates()...))
}

func HashString(hash [sha256.Size]byte) string {
return strings.TrimRight(base32.HexEncoding.EncodeToString(hash[:]), "=")
}

func CertificatesHash(certs ...*x509.Certificate) [sha256.Size]byte {
hash := sha256.New()
for _, cert := range certs {
_, _ = hash.Write(cert.Raw)
}
var certsHash [sha256.Size]byte
_ = hash.Sum(certsHash[:0])
return certsHash
}
4 changes: 2 additions & 2 deletions internal/pki/csr.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (
// publicKey is the public key of the signee, and signerKey is the private
// key of the signer.
// It returns a parsed *x509.Certificate on success.
func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey any) (*x509.Certificate, error) {
func SignCertificate(certParser *CertParser, template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey any) (*x509.Certificate, error) {
typedSigner, ok := signerKey.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("didn't get an expected Signer in call to SignCertificate")
Expand Down Expand Up @@ -77,7 +77,7 @@ func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, p
return nil, fmt.Errorf("error creating x509 certificate: %w", err)
}

cert, err := x509.ParseCertificate(derBytes)
cert, err := DecodeCertificateFromDER(certParser, derBytes)
if err != nil {
return nil, fmt.Errorf("error decoding DER certificate bytes: %w", err)
}
Expand Down
13 changes: 9 additions & 4 deletions internal/pki/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,28 @@ import (
)

// DecodeCertificateFromPEM will decode a PEM encoded x509 Certificate.
func DecodeCertificateFromPEM(certBytes []byte) (*x509.Certificate, error) {
func DecodeCertificateFromPEM(parser *CertParser, certBytes []byte) (*x509.Certificate, error) {
var returnedCert *x509.Certificate
return returnedCert, parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) {
return returnedCert, parser.parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) {
returnedCert = cert
return false, nil // stop after first cert, will error if there are more
})
}

// DecodeAllCertificatesFromPEM will decode a concatenated list of PEM encoded x509 Certificates.
func DecodeAllCertificatesFromPEM(certBytes []byte) ([]*x509.Certificate, error) {
func DecodeAllCertificatesFromPEM(parser *CertParser, certBytes []byte) ([]*x509.Certificate, error) {
var returnedCerts []*x509.Certificate
return returnedCerts, parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) {
return returnedCerts, parser.parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) {
returnedCerts = append(returnedCerts, cert)
return true, nil
})
}

// DecodeCertificateFromDER will decode a DER encoded x509 Certificate.
func DecodeCertificateFromDER(parser *CertParser, derBytes []byte) (*x509.Certificate, error) {
return parser.parseCertificateDER(derBytes)
}

// DecodePrivateKeyBytes will decode a PEM encoded private key into a crypto.Signer.
// It supports ECDSA and RSA private keys only. All other types will return err.
func DecodePrivateKeyBytes(keyBytes []byte) (crypto.Signer, error) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/authority/ca_secret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ func (r *CASecretReconciler) reconcileSecret(ctx context.Context, secret *corev1
if required, reason := caRequiresRegeneration(secret); required {
log.FromContext(ctx).Info("Will regenerate CA", "reason", reason)

caCert, caPk, err = certificate.GenerateCA(r.CAOptions.Duration)
caCert, caPk, err = certificate.GenerateCA(pki.NewCertParser(), r.CAOptions.Duration)
if err != nil {
return caCert, err
}
} else {
caCert, err = pki.DecodeCertificateFromPEM(secret.Data[corev1.TLSCertKey])
caCert, err = pki.DecodeCertificateFromPEM(pki.NewCertParser(), secret.Data[corev1.TLSCertKey])
if err != nil {
return caCert, err
}
Expand Down Expand Up @@ -136,7 +136,7 @@ func (r *CASecretReconciler) reconcileSecret(ctx context.Context, secret *corev1
func addCertToCABundle(ctx context.Context, caBundleBytes []byte, caCert *x509.Certificate) []byte {
certPool := pki.NewCertPool(pki.WithFilteredExpiredCerts(true))

if err := certPool.AddCertificatesFromPEM(caBundleBytes); err != nil {
if err := certPool.AddCertificatesFromPEM(pki.NewCertParser(), caBundleBytes); err != nil {
log.FromContext(ctx).Error(err, "failed to re-use existing CAs in new set of CAs")
}
// TODO: handle AddCertificate returning false? I expect this will never happen.
Expand Down
2 changes: 1 addition & 1 deletion pkg/authority/ca_secret_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func Test__caRequiresRegeneration(t *testing.T) {
if mod != nil {
mod(cert)
}
cert, err = pki.SignCertificate(cert, cert, pk.Public(), pk)
cert, err = pki.SignCertificate(pki.NewCertParser(), cert, cert, pk.Public(), pk)
assert.NoError(t, err)
certBytes, err := pki.EncodeCertificateAsPEM(cert)
assert.NoError(t, err)
Expand Down
3 changes: 2 additions & 1 deletion pkg/authority/leaf_cert_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (r *LeafCertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
}

func (r *LeafCertReconciler) generateCertificate(caSecret *corev1.Secret) (cert *x509.Certificate, pk crypto.Signer, err error) {
caCert, err := pki.DecodeCertificateFromPEM(caSecret.Data[corev1.TLSCertKey])
caCert, err := pki.DecodeCertificateFromPEM(pki.NewCertParser(), caSecret.Data[corev1.TLSCertKey])
if err != nil {
return cert, pk, err
}
Expand All @@ -79,6 +79,7 @@ func (r *LeafCertReconciler) generateCertificate(caSecret *corev1.Secret) (cert
}

cert, pk, err = certificate.GenerateLeaf(
pki.NewCertParser(),
r.LeafOptions.DNSNames,
r.LeafOptions.Duration,
caCert, caPk,
Expand Down
4 changes: 2 additions & 2 deletions test/ca_secret_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ var _ = Describe("CA Secret Controller", Ordered, func() {
It("should retain old CA if CA is rotated", func() {
assertCASecret(caSecret)

caBundleCerts, err := pki.DecodeAllCertificatesFromPEM(caSecret.Data[api.TLSCABundleKey])
caBundleCerts, err := pki.DecodeAllCertificatesFromPEM(pki.NewCertParser(), caSecret.Data[api.TLSCABundleKey])
Expect(err).ToNot(HaveOccurred())
Expect(caBundleCerts).To(HaveLen(1))

Expand All @@ -136,7 +136,7 @@ var _ = Describe("CA Secret Controller", Ordered, func() {
HaveField("Data", HaveKeyWithValue(corev1.TLSCertKey, Equal(certBytes))),
)

caBundleCerts, err = pki.DecodeAllCertificatesFromPEM(caSecret.Data[api.TLSCABundleKey])
caBundleCerts, err = pki.DecodeAllCertificatesFromPEM(pki.NewCertParser(), caSecret.Data[api.TLSCABundleKey])
Expect(err).ToNot(HaveOccurred())
Expect(caBundleCerts).To(HaveLen(2))
})
Expand Down
2 changes: 1 addition & 1 deletion test/leaf_cert_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var _ = Describe("Leaf Certificate Controller", Ordered, func() {
ns.Name = opts.CAOptions.Namespace
Expect(k8sClient.Create(ctx, ns)).To(Succeed())

caCert, caPK, err := certificate.GenerateCA(opts.CAOptions.Duration)
caCert, caPK, err := certificate.GenerateCA(pki.NewCertParser(), opts.CAOptions.Duration)
Expect(err).ToNot(HaveOccurred())
caCertBytes, err := pki.EncodeCertificateAsPEM(caCert)
Expect(err).ToNot(HaveOccurred())
Expand Down
6 changes: 3 additions & 3 deletions test/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ func assertCASecret(secret *corev1.Secret) {
)),
))

cert, err := pki.DecodeCertificateFromPEM(secret.Data[corev1.TLSCertKey])
cert, err := pki.DecodeCertificateFromPEM(pki.NewCertParser(), secret.Data[corev1.TLSCertKey])
Expect(err).ToNot(HaveOccurred())
caBundle, err := pki.DecodeAllCertificatesFromPEM(secret.Data[api.TLSCABundleKey])
caBundle, err := pki.DecodeAllCertificatesFromPEM(pki.NewCertParser(), secret.Data[api.TLSCABundleKey])
Expect(err).ToNot(HaveOccurred())

Expect(secretPublicKeysDiffer(secret)).To(BeFalse())
Expand Down Expand Up @@ -82,7 +82,7 @@ func secretPublicKeysDiffer(secret *corev1.Secret) (bool, error) {
if err != nil {
return true, fmt.Errorf("secret contains invalid private key data: %w", err)
}
x509Cert, err := pki.DecodeCertificateFromPEM(secret.Data[corev1.TLSCertKey])
x509Cert, err := pki.DecodeCertificateFromPEM(pki.NewCertParser(), secret.Data[corev1.TLSCertKey])
if err != nil {
return true, fmt.Errorf("secret contains an invalid certificate: %w", err)
}
Expand Down