Skip to content
Closed
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
144 changes: 144 additions & 0 deletions api/core/v1alpha2/tlscertificate_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package v1alpha2

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
)

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TLSCertificate represents a TLS certificate and private key pair.
// This matches the Kubernetes TLS secret type but is specific to that use case.
// The public/private key pair must exist beforehand.
// The public key certificate must be PEM encoded and match the given private key.
type TLSCertificate struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec TLSCertificateSpec `json:"spec,omitempty"`
Status TLSCertificateStatus `json:"status,omitempty"`
}

// TLSCertificateSpec defines the desired state of TLSCertificate
type TLSCertificateSpec struct {
// Certificate is the PEM-encoded TLS certificate.
// This should contain one or more certificate blocks.
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Certificate string `json:"certificate"`

// PrivateKey is the PEM-encoded private key corresponding to the certificate.
// This must match the public key in the certificate.
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
PrivateKey string `json:"privateKey"`
}

// TLSCertificateStatus defines the observed state of TLSCertificate
type TLSCertificateStatus struct {
// NotBefore is the time before which the certificate is not valid.
// +optional
NotBefore *metav1.Time `json:"notBefore,omitempty"`

// NotAfter is the time after which the certificate is not valid.
// +optional
NotAfter *metav1.Time `json:"notAfter,omitempty"`

// Issuer is the issuer of the certificate.
// +optional
Issuer string `json:"issuer,omitempty"`

// Subject is the subject of the certificate.
// +optional
Subject string `json:"subject,omitempty"`

// DNSNames is the list of DNS names in the certificate's Subject Alternative Names.
// +optional
DNSNames []string `json:"dnsNames,omitempty"`

// Conditions represent the latest available observations of the TLSCertificate's state.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TLSCertificateList contains a list of TLSCertificate
type TLSCertificateList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TLSCertificate `json:"items"`
}

// GetGroupVersionResource returns the GroupVersionResource for TLSCertificate.
func (in *TLSCertificate) GetGroupVersionResource() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: GroupVersion.Group,
Version: GroupVersion.Version,
Resource: "tlscertificates",
}
}

// GetObjectMeta returns the object metadata for TLSCertificate.
func (in *TLSCertificate) GetObjectMeta() *metav1.ObjectMeta {
return &in.ObjectMeta
}

// IsStorageVersion returns true if TLSCertificate is the storage version.
func (in *TLSCertificate) IsStorageVersion() bool {
return true
}

// NamespaceScoped returns false as TLSCertificate is cluster-scoped.
func (in *TLSCertificate) NamespaceScoped() bool {
return false
}

// New returns a new TLSCertificate.
func (in *TLSCertificate) New() runtime.Object {
return &TLSCertificate{}
}

// NewList returns a new TLSCertificateList.
func (in *TLSCertificate) NewList() runtime.Object {
return &TLSCertificateList{}
}

// GetStatus returns the status of the TLSCertificate.
func (in *TLSCertificate) GetStatus() resource.StatusSubResource {
return in.Status
}

// TLSCertificateStatus implements the StatusSubResource interface.
var _ resource.StatusSubResource = &TLSCertificateStatus{}

func (in TLSCertificateStatus) SubResourceName() string {
return "status"
}

// CopyTo copies the status to the given parent resource.
func (in TLSCertificateStatus) CopyTo(parent resource.ObjectWithStatusSubResource) {
parent.(*TLSCertificate).Status = in
}

var _ resource.Object = &TLSCertificate{}
var _ resource.ObjectList = &TLSCertificateList{}
var _ resource.ObjectWithStatusSubResource = &TLSCertificate{}

// GetListMeta returns the list metadata.
func (in *TLSCertificateList) GetListMeta() *metav1.ListMeta {
return &in.ListMeta
}

// TableConvertor returns a TableConvertor for TLSCertificate.
func (in *TLSCertificate) TableConvertor() rest.TableConvertor {
return nil
}
178 changes: 178 additions & 0 deletions api/core/v1alpha2/tlscertificate_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package v1alpha2

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource/resourcestrategy"
)

var _ resourcestrategy.Defaulter = &TLSCertificate{}
var _ resourcestrategy.Validater = &TLSCertificate{}
var _ resourcestrategy.ValidateUpdater = &TLSCertificate{}

// Default sets the default values for a TLSCertificate.
func (r *TLSCertificate) Default() {
// Parse certificate to populate status fields
if r.Spec.Certificate != "" {
if cert, err := parseCertificate(r.Spec.Certificate); err == nil {
r.Status.NotBefore = &metav1.Time{Time: cert.NotBefore}
r.Status.NotAfter = &metav1.Time{Time: cert.NotAfter}
r.Status.Issuer = cert.Issuer.String()
r.Status.Subject = cert.Subject.String()
r.Status.DNSNames = cert.DNSNames
}
}
}

func (r *TLSCertificate) Validate(ctx context.Context) field.ErrorList {
return r.validate()
}

func (r *TLSCertificate) ValidateUpdate(ctx context.Context, obj runtime.Object) field.ErrorList {
t := &TLSCertificate{}
// XXX: Conversion needs to happen in apiserver-runtime before validation hooks are called.
if mv, ok := obj.(resource.MultiVersionObject); ok {
mv.ConvertToStorageVersion(t)
} else if t, ok = obj.(*TLSCertificate); !ok {
return field.ErrorList{
field.Invalid(field.NewPath("kind"), obj.GetObjectKind().GroupVersionKind().Kind, "expected TLSCertificate"),
}
}

return t.validate()
}

func (r *TLSCertificate) validate() field.ErrorList {
errs := field.ErrorList{}

// Validate certificate is PEM encoded
cert, err := parseCertificate(r.Spec.Certificate)
if err != nil {
errs = append(errs, field.Invalid(
field.NewPath("spec", "certificate"),
"<redacted>",
fmt.Sprintf("invalid PEM-encoded certificate: %v", err),
))
return errs
}

// Validate private key is PEM encoded
privateKey, err := parsePrivateKey(r.Spec.PrivateKey)
if err != nil {
errs = append(errs, field.Invalid(
field.NewPath("spec", "privateKey"),
"<redacted>",
fmt.Sprintf("invalid PEM-encoded private key: %v", err),
))
return errs
}

// Validate that the private key matches the certificate's public key
if err := validateKeyPair(cert, privateKey); err != nil {
errs = append(errs, field.Invalid(
field.NewPath("spec", "privateKey"),
"<redacted>",
fmt.Sprintf("private key does not match certificate public key: %v", err),
))
}

return errs
}

// parseCertificate parses a PEM-encoded certificate and returns the first certificate found.
func parseCertificate(certPEM string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(certPEM))
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block containing certificate")
}

if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("PEM block type must be CERTIFICATE, got %s", block.Type)
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}

return cert, nil
}

// parsePrivateKey parses a PEM-encoded private key and returns the private key.
// Supports RSA, ECDSA, and Ed25519 keys.
func parsePrivateKey(keyPEM string) (crypto.PrivateKey, error) {
block, _ := pem.Decode([]byte(keyPEM))
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block containing private key")
}

// Try parsing as PKCS8 first (most common format)
if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
return key, nil
}

// Try parsing as PKCS1 RSA private key
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return key, nil
}

// Try parsing as EC private key
if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil {
return key, nil
}

return nil, fmt.Errorf("failed to parse private key: unsupported key type or format")
}

// validateKeyPair validates that the private key matches the certificate's public key.
func validateKeyPair(cert *x509.Certificate, privateKey crypto.PrivateKey) error {
switch pub := cert.PublicKey.(type) {
case *rsa.PublicKey:
priv, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("certificate has RSA public key but private key is not RSA")
}
if pub.N.Cmp(priv.N) != 0 || pub.E != priv.E {
return fmt.Errorf("RSA private key does not match certificate public key")
}

case *ecdsa.PublicKey:
priv, ok := privateKey.(*ecdsa.PrivateKey)
if !ok {
return fmt.Errorf("certificate has ECDSA public key but private key is not ECDSA")
}
if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 {
return fmt.Errorf("ECDSA private key does not match certificate public key")
}

case ed25519.PublicKey:
priv, ok := privateKey.(ed25519.PrivateKey)
if !ok {
return fmt.Errorf("certificate has Ed25519 public key but private key is not Ed25519")
}
// Ed25519 private key contains the public key in the last 32 bytes
if len(priv) != ed25519.PrivateKeySize {
return fmt.Errorf("invalid Ed25519 private key size")
}
derivedPub := priv.Public().(ed25519.PublicKey)
if !derivedPub.Equal(pub) {
return fmt.Errorf("Ed25519 private key does not match certificate public key")
}

default:
return fmt.Errorf("unsupported public key type: %T", pub)
}

return nil
}
Loading