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
7 changes: 6 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ linters:
- unconvert
- unparam
- unused
exclusions:
rules:
- linters:
- lll
source: "^// \\+kubebuilder:validation"

formatters:
enable:
- gofmt
- goimports
- goimports
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Change Log
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
-

### Changed

### Fixed

## [x.x.x]

67 changes: 47 additions & 20 deletions api/v1alpha1/modelvalidation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,65 @@ import (
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// Model defines the details of the model to validate.
// Model defines the details of the model to validate, including its path and
// the path to its corresponding signature file.
type Model struct {
Path string `json:"path"`
// Path to the model artifact. This could be a file path on a shared volume
// or a URI to an object store.
// +kubebuilder:validation:Pattern=`^(/|s3://|gs://|https?://)`
Path string `json:"path"`
// SignaturePath is the path to the cryptographic signature bundle for the model.
// This is used by the various validation methods to verify the model's integrity.
// +kubebuilder:validation:Pattern=`^(/|s3://|gs://|https?://)`
SignaturePath string `json:"signaturePath"`
}

// SigstoreConfig defines the Sigstore verification configuration
// for validating model signatures using certificate identity and OIDC issuer
// SigstoreConfig defines the configuration for validating model signatures using
// Sigstore's certificate-based method, which requires a specific certificate
// identity and OIDC issuer.
type SigstoreConfig struct {
CertificateIdentity string `json:"certificateIdentity,omitempty"`
// CertificateIdentity is the expected identity of the signing certificate.
// For example, "email:[email protected]".
// +kubebuilder:validation:Required
CertificateIdentity string `json:"certificateIdentity,omitempty"`
// CertificateOidcIssuer is the URL of the OIDC issuer that issued the signing certificate.
// For example, "https://accounts.google.com".
// +kubebuilder:validation:Required
CertificateOidcIssuer string `json:"certificateOidcIssuer,omitempty"`
}

// PkiConfig defines the PKI-based verification configuration
// using a certificate authority for validating model signatures
// PkiConfig defines the configuration for PKI-based verification.
// This method validates the signature using a trusted certificate authority (CA).
type PkiConfig struct {
// Path to the certificate authority for PKI.
// CertificateAuthorityPath is the path to the trusted certificate authority (CA) file.
// The signature's chain of trust will be verified against this CA.
// +kubebuilder:validation:Required
CertificateAuthority string `json:"certificateAuthority,omitempty"`
}

// PrivateKeyConfig defines the private key verification configuration
// for validating model signatures using a local private key
type PrivateKeyConfig struct {
// Path to the private key.
// PublicKeyConfig defines the configuration for public key-based verification.
// This method validates the signature directly against a public key.
type PublicKeyConfig struct {
// KeyPath is the file path to the public key used for signature verification.
// This should be the public key corresponding to the private key used for signing.
// +kubebuilder:validation:Required
KeyPath string `json:"keyPath,omitempty"`
}

// ValidationConfig defines the various methods available for validating model signatures.
// At least one validation method must be specified.
// Only one validation method should be specified. The controller will use the first
// non-nil method it finds.
// +kubebuilder:validation:XValidation:rule="[has(self.sigstoreConfig), has(self.pkiConfig), has(self.publicKeyConfig)].filter(x, x).size() == 1", message="exactly one validation method must be specified"
type ValidationConfig struct {
SigstoreConfig *SigstoreConfig `json:"sigstoreConfig,omitempty"`
PkiConfig *PkiConfig `json:"pkiConfig,omitempty"`
PrivateKeyConfig *PrivateKeyConfig `json:"privateKeyConfig,omitempty"`
// SigstoreConfig is the configuration for Sigstore-based signature verification.
// +kubebuilder:validation:Optional
SigstoreConfig *SigstoreConfig `json:"sigstoreConfig,omitempty"`
// +kubebuilder:validation:Optional
// PkiConfig is the configuration for traditional PKI-based signature verification.
PkiConfig *PkiConfig `json:"pkiConfig,omitempty"`
// +kubebuilder:validation:Optional
// PublicKeyConfig is the configuration for public key-based signature verification.
PublicKeyConfig *PublicKeyConfig `json:"publicKeyConfig,omitempty"`
}

// ModelValidationSpec defines the desired state of ModelValidation
Expand All @@ -66,20 +92,21 @@ type ModelValidationSpec struct {
// Model details.
Model Model `json:"model"`
// Configuration for validation methods.
// Exactly one validation method must be specified.
Config ValidationConfig `json:"config"`
}

// ModelValidationStatus defines the observed state of ModelValidation
type ModelValidationStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// ModelValidation is the Schema for the modelvalidations API.
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// ModelValidation is the Schema for the modelvalidations API
// +kubebuilder:resource:shortName=mv
// +kubebuilder:printcolumn:name="Model Path",type="string",JSONPath=".spec.model.path"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type ModelValidation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
233 changes: 233 additions & 0 deletions api/v1alpha1/modelvalidation_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package v1alpha1

import (
"context"

. "github.com/onsi/ginkgo/v2" //nolint:revive
. "github.com/onsi/gomega" //nolint:revive
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = Describe("ModelValidation", func() {

// A helper function to generate a valid ModelValidation object for Sigstore.
generateSigstoreObject := func(name string) *ModelValidation {
return &ModelValidation{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: ModelValidationSpec{
Model: Model{
Path: "/path/to/model.onnx",
SignaturePath: "/path/to/model.onnx.sig",
},
Config: ValidationConfig{
SigstoreConfig: &SigstoreConfig{
CertificateIdentity: "email:[email protected]",
CertificateOidcIssuer: "https://accounts.google.com",
},
},
},
}
}

// A helper function to generate a valid ModelValidation object for PKI.
generatePkiObject := func(name string) *ModelValidation {
return &ModelValidation{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: ModelValidationSpec{
Model: Model{
Path: "/path/to/model.onnx",
SignaturePath: "/path/to/model.onnx.sig",
},
Config: ValidationConfig{
PkiConfig: &PkiConfig{
CertificateAuthority: "/path/to/ca.pem",
},
},
},
}
}

// A helper function to generate a valid ModelValidation object for PublicKey.
generatePublicKeyObject := func(name string) *ModelValidation {
return &ModelValidation{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: ModelValidationSpec{
Model: Model{
Path: "/path/to/model.onnx",
SignaturePath: "/path/to/model.onnx.sig",
},
Config: ValidationConfig{
PublicKeyConfig: &PublicKeyConfig{
KeyPath: "/path/to/publickey.pem",
},
},
},
}
}

Context("ModelValidationSpec", func() {
It("can be created and fetched successfully for Sigstore config", func() {
created := generateSigstoreObject("mv-create")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())

fetched := &ModelValidation{}
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())
Expect(fetched).To(Equal(created))
})

It("can be created and fetched successfully for PKI config", func() {
created := generatePkiObject("mv-create-pki")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
})

It("can be created and fetched successfully for PublicKey config", func() {
created := generatePublicKeyObject("mv-create-publickey")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
})

It("can be updated with allowed fields", func() {
created := generateSigstoreObject("mv-update")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())

fetched := &ModelValidation{}
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())
Expect(fetched).To(Equal(created))

// Status is not immutable and can be updated
fetched.Status.Conditions = []metav1.Condition{
{
Type: "Ready",
Status: "True",
LastTransitionTime: metav1.Now(),
Reason: "ValidationSuccess",
Message: "Model signature is valid",
},
}
Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed())
})

It("can be deleted", func() {
created := generateSigstoreObject("mv-delete")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())

Expect(k8sClient.Delete(context.Background(), created)).To(Succeed())
Expect(k8sClient.Get(context.Background(), getKey(created), created)).ToNot(Succeed())
})

Context("is validated", func() {
It("rejects an empty Model path", func() {
invalidObject := generateSigstoreObject("model-path-invalid")
invalidObject.Spec.Model.Path = ""

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("spec.model.path: Invalid value: \"\"")))
})

It("rejects an empty Signature path", func() {
invalidObject := generateSigstoreObject("signature-path-invalid")
invalidObject.Spec.Model.SignaturePath = ""

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("spec.model.signaturePath: Invalid value: \"\"")))
})

It("rejects multiple configs (XValidation violation)", func() {
invalidObject := generateSigstoreObject("xor-violation-multi")
invalidObject.Spec.Config.PkiConfig = &PkiConfig{
CertificateAuthority: "/path/to/ca.pem",
}

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("exactly one validation method must be specified")))
})

It("rejects zero configs (XValidation violation)", func() {
invalidObject := generateSigstoreObject("xor-violation-zero")
invalidObject.Spec.Config.SigstoreConfig = nil

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("exactly one validation method must be specified")))
})

It("rejects a missing required field in SigstoreConfig", func() {
invalidObject := generateSigstoreObject("sigstore-missing-field")
invalidObject.Spec.Config.SigstoreConfig.CertificateIdentity = ""

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("spec.config.sigstoreConfig.certificateIdentity: Required value")))
})

It("rejects a missing required field in PkiConfig", func() {
invalidObject := generatePkiObject("pki-missing-field")
invalidObject.Spec.Config.PkiConfig.CertificateAuthority = ""

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("spec.config.pkiConfig.certificateAuthority: Required value")))
})

It("rejects a missing required field in PublicKeyConfig", func() {
invalidObject := generatePublicKeyObject("publickey-missing-field")
invalidObject.Spec.Config.PublicKeyConfig.KeyPath = ""

err := k8sClient.Create(context.Background(), invalidObject)
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring("spec.config.publicKeyConfig.keyPath: Required value")))
})

It("allows an update to the Model path", func() {
created := generateSigstoreObject("mutable-model-test")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())

fetched := &ModelValidation{}
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())

newPath := "/new/path/to/model.onnx"
fetched.Spec.Model.Path = newPath
Expect(k8sClient.Update(context.Background(), fetched)).To(Succeed())

// Fetch and verify the change
updated := &ModelValidation{}
Expect(k8sClient.Get(context.Background(), getKey(created), updated)).To(Succeed())
Expect(updated.Spec.Model.Path).To(Equal(newPath))
})

It("allows an update to the Config fields", func() {
created := generateSigstoreObject("mutable-config-test")
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())

fetched := &ModelValidation{}
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())

// Update the config from Sigstore to PKI
fetched.Spec.Config.SigstoreConfig = nil
fetched.Spec.Config.PkiConfig = &PkiConfig{
CertificateAuthority: "new-ca-path",
}
Expect(k8sClient.Update(context.Background(), fetched)).To(Succeed())

// Fetch and verify the change
updated := &ModelValidation{}
Expect(k8sClient.Get(context.Background(), getKey(created), updated)).To(Succeed())
Expect(updated.Spec.Config.SigstoreConfig).To(BeNil())
Expect(updated.Spec.Config.PkiConfig).ToNot(BeNil())
Expect(updated.Spec.Config.PkiConfig.CertificateAuthority).To(Equal("new-ca-path"))
})
})
})
})
Loading