diff --git a/internal/controller/constants/labels.go b/internal/controller/constants/labels.go index c6d3aa709..2b02c4f96 100644 --- a/internal/controller/constants/labels.go +++ b/internal/controller/constants/labels.go @@ -39,6 +39,33 @@ func LabelsRHTAS() map[string]string { } } +func AddLabel(ctx context.Context, object *metav1.PartialObjectMetadata, c client.Client, label string, value string) error { + object.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Secret", + }) + patch, err := json.Marshal([]map[string]any{ + { + "op": "add", + "path": "/metadata/labels", + "value": map[string]string{ + label: value, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to marshal patch: %v", err) + } + + err = c.Patch(ctx, object, client.RawPatch(types.JSONPatchType, patch)) + if err != nil { + return fmt.Errorf("unable to add '%s' label to object: %w", label, err) + } + + return nil +} + func RemoveLabel(ctx context.Context, object *metav1.PartialObjectMetadata, c client.Client, label string) error { object.SetGroupVersionKind(schema.GroupVersionKind{ Group: "", diff --git a/internal/controller/ctlog/actions/constants.go b/internal/controller/ctlog/actions/constants.go index 5ead8d88d..e8a06b442 100644 --- a/internal/controller/ctlog/actions/constants.go +++ b/internal/controller/ctlog/actions/constants.go @@ -8,13 +8,17 @@ const ( RBACName = "ctlog" MonitoringRoleName = "prometheus-k8s-ctlog" - CertCondition = "FulcioCertAvailable" + SignerCondition = "SignerAvailable" + CertCondition = "FulcioCertAvailable" + ServerConfigCondition = "ServerConfigAvailable" + PublicKeyCondition = "PublicKeyAvailable" + TreeCondition = "TreeAvailable" + ServerPortName = "http" ServerPort = 80 ServerTargetPort = 6962 MetricsPortName = "metrics" MetricsPort = 6963 - ServerCondition = "ServerAvailable" CTLPubLabel = constants.LabelNamespace + "/ctfe.pub" ) diff --git a/internal/controller/ctlog/actions/generate_signer.go b/internal/controller/ctlog/actions/generate_signer.go new file mode 100644 index 000000000..965a69343 --- /dev/null +++ b/internal/controller/ctlog/actions/generate_signer.go @@ -0,0 +1,151 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/securesign/operator/internal/controller/ctlog/utils" + + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/constants" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const KeySecretNameFormat = "ctlog-%s-keys-" + +var ( + ErrGenerateSignerKey = errors.New("failed to generate signer key") + ErrParseSignerKey = errors.New("failed to parse signer key") +) + +func NewGenerateSignerAction() action.Action[*v1alpha1.CTlog] { + return &generateSigner{} +} + +type generateSigner struct { + action.BaseAction +} + +func (g generateSigner) Name() string { + return "generate-signer" +} + +func (g generateSigner) CanHandle(_ context.Context, instance *v1alpha1.CTlog) bool { + + if instance.Status.PrivateKeyRef == nil { + return true + } + + if !equality.Semantic.DeepDerivative(instance.Spec.PrivateKeyRef, instance.Status.PrivateKeyRef) { + return true + } + + if !equality.Semantic.DeepDerivative(instance.Spec.PrivateKeyPasswordRef, instance.Status.PrivateKeyPasswordRef) { + return true + } + + return !meta.IsStatusConditionTrue(instance.Status.Conditions, SignerCondition) +} + +func (g generateSigner) Handle(ctx context.Context, instance *v1alpha1.CTlog) *action.Result { + // Force to change SignerCondition when spec has changed + if meta.IsStatusConditionTrue(instance.Status.Conditions, SignerCondition) { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: SignerCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "resolving signer key", + }) + return g.StatusUpdate(ctx, instance) + } + + if instance.Spec.PrivateKeyRef != nil { + instance.Status.PrivateKeyRef = instance.Spec.PrivateKeyRef + if instance.Spec.PrivateKeyPasswordRef != nil { + instance.Status.PrivateKeyPasswordRef = instance.Spec.PrivateKeyPasswordRef + } + + g.Recorder.Eventf(instance, v1.EventTypeNormal, "SignerKeyCreated", "Using signer key from `%s` secret", instance.Spec.PrivateKeyRef.Name) + } else { + + var ( + err error + ) + + config, err := utils.NewSignerConfig(utils.WithGeneratedKey()) + if err != nil { + return g.Failed(fmt.Errorf("%w: %w", ErrGenerateSignerKey, err)) + } + + labels := constants.LabelsFor(ComponentName, DeploymentName, instance.Name) + + privateKey, err := config.PrivateKeyPEM() + if err != nil { + return g.Failed(fmt.Errorf("%w, %w", ErrParseSignerKey, err)) + } + password := config.PrivateKeyPassword() + + data := map[string][]byte{ + "private": privateKey, + "password": password, + } + + secret := k8sutils.CreateImmutableSecret(fmt.Sprintf(KeySecretNameFormat, instance.Name), instance.Namespace, + data, labels) + if _, err = g.Ensure(ctx, secret); err != nil { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: SignerCondition, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return g.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create secret: %w", err), instance) + } + g.Recorder.Eventf(instance, v1.EventTypeNormal, "SignerKeyCreated", "Signer private key created: %s", secret.Name) + + instance.Status.PrivateKeyRef = &v1alpha1.SecretKeySelector{ + Key: "private", + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: secret.Name, + }, + } + + if len(secret.Data["password"]) > 0 && instance.Spec.PrivateKeyPasswordRef == nil { + instance.Status.PrivateKeyPasswordRef = &v1alpha1.SecretKeySelector{ + Key: "password", + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: secret.Name, + }, + } + } else { + instance.Status.PrivateKeyPasswordRef = instance.Spec.PrivateKeyPasswordRef + } + } + + // invalidate server config + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: ServerConfigCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "signer key changed", + }) + // invalidate public key + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "signer key changed", + }) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }) + return g.StatusUpdate(ctx, instance) +} diff --git a/internal/controller/ctlog/actions/generate_signer_test.go b/internal/controller/ctlog/actions/generate_signer_test.go new file mode 100644 index 000000000..fc7351983 --- /dev/null +++ b/internal/controller/ctlog/actions/generate_signer_test.go @@ -0,0 +1,358 @@ +package actions + +import ( + "context" + + . "github.com/onsi/gomega" + + "reflect" + "testing" + + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/constants" + testAction "github.com/securesign/operator/internal/testing/action" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestGenerateSigner_CanHandle(t *testing.T) { + tests := []struct { + name string + status []metav1.Condition + canHandle bool + privateKeyRef *rhtasv1alpha1.SecretKeySelector + statusPrivateKeyRef *rhtasv1alpha1.SecretKeySelector + privateKeyPassRef *rhtasv1alpha1.SecretKeySelector + statusPrivateKeyPassRef *rhtasv1alpha1.SecretKeySelector + }{ + { + name: "spec.privateKeyRef is not nil and status.privateKeyRef is nil", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + privateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + statusPrivateKeyRef: nil, + }, + { + name: "spec.privateKeyRef is nil and status.privateKeyRef is not nil", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: false, + privateKeyRef: nil, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + }, + { + name: "spec.privateKeyRef is nil and status.privateKeyRef is nil", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + privateKeyRef: nil, + statusPrivateKeyRef: nil, + }, + { + name: "spec.privateKeyRef != status.privateKeyRef", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + privateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "new_secret"}, Key: "private"}, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old_secret"}, Key: "private"}, + }, + { + name: "spec.privateKeyRef == status.privateKeyRef", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: false, + privateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + }, + { + name: "spec.privateKeyPasswordRef == status.privateKeyPasswordRef", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: false, + privateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + privateKeyPassRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "pass"}, + statusPrivateKeyPassRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "pass"}, + }, + { + name: "spec.privateKeyPasswordRef != status.privateKeyPasswordRef", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + privateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + privateKeyPassRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "new_secret"}, Key: "pass"}, + statusPrivateKeyPassRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old_secret"}, Key: "pass"}, + }, + { + name: "no phase condition", + status: []metav1.Condition{}, + canHandle: true, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + }, + { + name: "ConditionFalse", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + }, + }, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + canHandle: true, + }, + { + name: "ConditionTrue", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + canHandle: false, + }, + { + name: "ConditionUnknown", + status: []metav1.Condition{ + { + Type: SignerCondition, + Status: metav1.ConditionUnknown, + Reason: constants.Ready, + }, + }, + statusPrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + canHandle: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := testAction.FakeClientBuilder().Build() + a := testAction.PrepareAction(c, NewGenerateSignerAction()) + instance := rhtasv1alpha1.CTlog{ + Spec: rhtasv1alpha1.CTlogSpec{ + PrivateKeyRef: tt.privateKeyRef, + PrivateKeyPasswordRef: tt.privateKeyPassRef, + }, + Status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: tt.statusPrivateKeyRef, + PrivateKeyPasswordRef: tt.statusPrivateKeyPassRef, + }, + } + for _, status := range tt.status { + meta.SetStatusCondition(&instance.Status.Conditions, status) + } + + if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { + t.Errorf("CanHandle() = %v, want %v", got, tt.canHandle) + } + }) + } +} + +func TestGenerateSigner_Handle(t *testing.T) { + g := NewWithT(t) + type env struct { + spec rhtasv1alpha1.CTlogSpec + status rhtasv1alpha1.CTlogStatus + objects []client.Object + } + type want struct { + result *action.Result + verify func(Gomega, *rhtasv1alpha1.CTlog) + } + tests := []struct { + name string + env env + want want + }{ + { + name: "use spec.privateKeyRef", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + }, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: nil, + }, + objects: []client.Object{ + kubernetes.CreateSecret("secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PrivateKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PrivateKeyRef.Name).Should(Equal("secret")) + g.Expect(instance.Status.PrivateKeyRef.Key).Should(Equal("private")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, SignerCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "generate private key", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{}, + status: rhtasv1alpha1.CTlogStatus{}, + objects: []client.Object{}, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PrivateKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PrivateKeyRef.Name).Should(ContainSubstring("ctlog-ctlog-keys-")) + + g.Expect(instance.Status.PrivateKeyPasswordRef).Should(BeNil()) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, SignerCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "replace status.privateKeyRef from spec", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "new_secret"}, Key: "private"}, + }, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old_secret"}, Key: "private"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("new_secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PrivateKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PrivateKeyRef.Name).Should(Equal("new_secret")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, SignerCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "spec with encrypted private key", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + PrivateKeyPasswordRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "password"}, + }, + status: rhtasv1alpha1.CTlogStatus{}, + objects: []client.Object{ + kubernetes.CreateSecret("secret", "default", map[string][]byte{ + "private": privatePassKey, + "password": []byte("changeit"), + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PrivateKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PrivateKeyRef.Name).Should(Equal("secret")) + g.Expect(instance.Status.PrivateKeyRef.Key).Should(Equal("private")) + + g.Expect(instance.Status.PrivateKeyPasswordRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PrivateKeyPasswordRef.Name).Should(Equal("secret")) + g.Expect(instance.Status.PrivateKeyPasswordRef.Key).Should(Equal("password")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, SignerCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + instance := &rhtasv1alpha1.CTlog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ctlog", + Namespace: "default", + }, + Spec: tt.env.spec, + Status: tt.env.status, + } + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Reason: constants.Pending, + }) + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + }) + + c := testAction.FakeClientBuilder(). + WithObjects(instance). + WithStatusSubresource(instance). + WithObjects(tt.env.objects...). + Build() + + a := testAction.PrepareAction(c, NewGenerateSignerAction()) + + if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { + t.Errorf("CanHandle() = %v, want %v", got, tt.want.result) + } + if tt.want.verify != nil { + tt.want.verify(g, instance) + } + }) + } +} diff --git a/internal/controller/ctlog/actions/handle_fulcio_root.go b/internal/controller/ctlog/actions/handle_fulcio_root.go index 672d61a8c..140a5df1a 100644 --- a/internal/controller/ctlog/actions/handle_fulcio_root.go +++ b/internal/controller/ctlog/actions/handle_fulcio_root.go @@ -9,7 +9,6 @@ import ( k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/constants" "github.com/securesign/operator/internal/controller/fulcio/actions" - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" k8sErrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -30,7 +29,7 @@ func (g handleFulcioCert) Name() string { func (g handleFulcioCert) CanHandle(ctx context.Context, instance *v1alpha1.CTlog) bool { c := meta.FindStatusCondition(instance.GetConditions(), constants.Ready) - if c.Reason != constants.Creating && c.Reason != constants.Ready { + if c.Reason != constants.Pending && c.Reason != constants.Ready { return false } @@ -57,13 +56,18 @@ func (g handleFulcioCert) CanHandle(ctx context.Context, instance *v1alpha1.CTlo func (g handleFulcioCert) Handle(ctx context.Context, instance *v1alpha1.CTlog) *action.Result { - if meta.FindStatusCondition(instance.Status.Conditions, constants.Ready).Reason != constants.Creating { + if meta.FindStatusCondition(instance.Status.Conditions, constants.Ready).Reason != constants.Pending { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: CertCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "discovering root certificates", + }) meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: constants.Ready, Status: metav1.ConditionFalse, - Reason: constants.Creating, - }, - ) + Reason: constants.Pending, + }) return g.StatusUpdate(ctx, instance) } @@ -96,25 +100,16 @@ func (g handleFulcioCert) Handle(ctx context.Context, instance *v1alpha1.CTlog) } // invalidate server config - if instance.Status.ServerConfigRef != nil { - if err := g.Client.Delete(ctx, &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: instance.Status.ServerConfigRef.Name, - Namespace: instance.Namespace, - }, - }); err != nil { - if !k8sErrors.IsNotFound(err) { - return g.Failed(err) - } - } - instance.Status.ServerConfigRef = nil - } - + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: ServerConfigCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "root certificates changed", + }) meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: CertCondition, Status: metav1.ConditionTrue, - Reason: "Resolved", - }, - ) + Reason: constants.Ready, + }) return g.StatusUpdate(ctx, instance) } diff --git a/internal/controller/ctlog/actions/handle_fulcio_root_test.go b/internal/controller/ctlog/actions/handle_fulcio_root_test.go index 33c522d68..485748611 100644 --- a/internal/controller/ctlog/actions/handle_fulcio_root_test.go +++ b/internal/controller/ctlog/actions/handle_fulcio_root_test.go @@ -12,7 +12,6 @@ import ( "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/constants" "github.com/securesign/operator/internal/controller/fulcio/actions" - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -31,7 +30,7 @@ func Test_HandleFulcioCert_Autodiscover(t *testing.T) { Conditions: []metav1.Condition{ { Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, Status: metav1.ConditionFalse, }, }, @@ -74,7 +73,7 @@ func Test_HandleFulcioCert_Empty(t *testing.T) { Conditions: []metav1.Condition{ { Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, Status: metav1.ConditionFalse, }, }, @@ -122,7 +121,7 @@ func Test_HandleFulcioCert_Configured(t *testing.T) { Conditions: []metav1.Condition{ { Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, Status: metav1.ConditionFalse, }, }, @@ -173,7 +172,7 @@ func Test_HandleFulcioCert_Configured_Priority(t *testing.T) { Conditions: []metav1.Condition{ { Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, Status: metav1.ConditionFalse, }, }, @@ -204,7 +203,7 @@ func Test_HandleFulcioCert_Configured_Priority(t *testing.T) { g.Expect(meta.IsStatusConditionTrue(i.Status.Conditions, CertCondition)).To(BeTrue()) } -func Test_HandleFulcioCert_Delete_ServerConfig(t *testing.T) { +func Test_HandleFulcioCert_Invalidate_ServerConfig(t *testing.T) { g := NewWithT(t) instance := &v1alpha1.CTlog{ @@ -225,7 +224,7 @@ func Test_HandleFulcioCert_Delete_ServerConfig(t *testing.T) { Conditions: []metav1.Condition{ { Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, Status: metav1.ConditionFalse, }, }, @@ -247,7 +246,5 @@ func Test_HandleFulcioCert_Delete_ServerConfig(t *testing.T) { _ = a.Handle(context.TODO(), i) g.Expect(meta.IsStatusConditionTrue(i.Status.Conditions, CertCondition)).To(BeTrue()) - - g.Expect(i.Status.ServerConfigRef).To(BeNil()) - g.Expect(c.Get(context.TODO(), types.NamespacedName{Name: "ctlog-config", Namespace: instance.GetNamespace()}, &v1.Secret{})).To(HaveOccurred()) + g.Expect(meta.IsStatusConditionFalse(i.Status.Conditions, ServerConfigCondition)).To(BeTrue()) } diff --git a/internal/controller/ctlog/actions/handle_keys.go b/internal/controller/ctlog/actions/handle_keys.go deleted file mode 100644 index d52aaaf19..000000000 --- a/internal/controller/ctlog/actions/handle_keys.go +++ /dev/null @@ -1,189 +0,0 @@ -package actions - -import ( - "context" - "fmt" - - "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common/action" - k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" - "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/ctlog/utils" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" - k8sErrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -const KeySecretNameFormat = "ctlog-%s-keys-" - -func NewHandleKeysAction() action.Action[*v1alpha1.CTlog] { - return &handleKeys{} -} - -type handleKeys struct { - action.BaseAction -} - -func (g handleKeys) Name() string { - return "handle-keys" -} - -func (g handleKeys) CanHandle(ctx context.Context, instance *v1alpha1.CTlog) bool { - c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) - if c.Reason != constants.Creating && c.Reason != constants.Ready { - return false - } - - return instance.Status.PrivateKeyRef == nil || instance.Status.PublicKeyRef == nil || - !equality.Semantic.DeepDerivative(instance.Spec.PrivateKeyRef, instance.Status.PrivateKeyRef) || - !equality.Semantic.DeepDerivative(instance.Spec.PublicKeyRef, instance.Status.PublicKeyRef) || - !equality.Semantic.DeepDerivative(instance.Spec.PrivateKeyPasswordRef, instance.Status.PublicKeyRef) -} - -func (g handleKeys) Handle(ctx context.Context, instance *v1alpha1.CTlog) *action.Result { - if meta.FindStatusCondition(instance.Status.Conditions, constants.Ready).Reason != constants.Creating { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: metav1.ConditionFalse, - Reason: constants.Creating, - }, - ) - return g.StatusUpdate(ctx, instance) - } - var ( - data map[string][]byte - ) - - if instance.Spec.PrivateKeyRef == nil { - config, err := utils.CreatePrivateKey() - if err != nil { - return g.Failed(err) - } - data = map[string][]byte{ - "private": config.PrivateKey, - "public": config.PublicKey, - } - } else { - var ( - private, password []byte - err error - config *utils.PrivateKeyConfig - ) - - private, err = k8sutils.GetSecretData(g.Client, instance.Namespace, instance.Spec.PrivateKeyRef) - if err != nil { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: metav1.ConditionFalse, - Reason: constants.Pending, - Message: "Waiting for secret " + instance.Spec.PrivateKeyRef.Name, - }) - g.StatusUpdate(ctx, instance) - // busy waiting - no watch on provided secrets - return g.Requeue() - } - if instance.Spec.PrivateKeyPasswordRef != nil { - password, err = k8sutils.GetSecretData(g.Client, instance.Namespace, instance.Spec.PrivateKeyPasswordRef) - if err != nil { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: constants.Creating, - Reason: constants.Pending, - Message: "Waiting for secret " + instance.Spec.PrivateKeyPasswordRef.Name, - }) - g.StatusUpdate(ctx, instance) - // busy waiting - no watch on provided secrets - return g.Requeue() - } - } - config, err = utils.GeneratePublicKey(&utils.PrivateKeyConfig{PrivateKey: private, PrivateKeyPass: password}) - if err != nil || config == nil { - return g.Failed(fmt.Errorf("unable to generate public key: %w", err)) - } - data = map[string][]byte{"public": config.PublicKey} - } - - labels := constants.LabelsFor(ComponentName, DeploymentName, instance.Name) - labels[CTLPubLabel] = "public" - secret := k8sutils.CreateImmutableSecret(fmt.Sprintf(KeySecretNameFormat, instance.Name), instance.Namespace, - data, labels) - - if err := controllerutil.SetControllerReference(instance, secret, g.Client.Scheme()); err != nil { - return g.Failed(fmt.Errorf("could not set controller reference for Secret: %w", err)) - } - - // ensure that only new key is exposed - if err := g.Client.DeleteAllOf(ctx, &v1.Secret{}, client.InNamespace(instance.Namespace), client.MatchingLabels(constants.LabelsFor(ComponentName, DeploymentName, instance.Name)), client.HasLabels{CTLPubLabel}); err != nil { - return g.Failed(err) - } - - if _, err := g.Ensure(ctx, secret); err != nil { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), - }) - return g.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create Secret: %w", err), instance) - } - - if instance.Spec.PrivateKeyRef == nil { - instance.Status.PrivateKeyRef = &v1alpha1.SecretKeySelector{ - Key: "private", - LocalObjectReference: v1alpha1.LocalObjectReference{ - Name: secret.Name, - }, - } - } else { - instance.Status.PrivateKeyRef = instance.Spec.PrivateKeyRef - } - - if _, ok := data["password"]; instance.Spec.PrivateKeyPasswordRef == nil && ok { - instance.Status.PrivateKeyPasswordRef = &v1alpha1.SecretKeySelector{ - Key: "password", - LocalObjectReference: v1alpha1.LocalObjectReference{ - Name: secret.Name, - }, - } - } else { - instance.Status.PrivateKeyPasswordRef = instance.Spec.PrivateKeyPasswordRef - } - - if instance.Spec.PublicKeyRef == nil { - instance.Status.PublicKeyRef = &v1alpha1.SecretKeySelector{ - Key: "public", - LocalObjectReference: v1alpha1.LocalObjectReference{ - Name: secret.Name, - }, - } - } else { - instance.Status.PublicKeyRef = instance.Spec.PublicKeyRef - } - - // invalidate server config - if instance.Status.ServerConfigRef != nil { - if err := g.Client.Delete(ctx, &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: instance.Status.ServerConfigRef.Name, - Namespace: instance.Namespace, - }, - }); err != nil { - if !k8sErrors.IsNotFound(err) { - return g.Failed(err) - } - } - instance.Status.ServerConfigRef = nil - } - - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: metav1.ConditionFalse, - Reason: constants.Creating, - Message: "Keys resolved", - }) - return g.StatusUpdate(ctx, instance) -} diff --git a/internal/controller/ctlog/actions/resolve_pub_key.go b/internal/controller/ctlog/actions/resolve_pub_key.go new file mode 100644 index 000000000..b8e7ff246 --- /dev/null +++ b/internal/controller/ctlog/actions/resolve_pub_key.go @@ -0,0 +1,209 @@ +package actions + +import ( + "bytes" + "context" + "errors" + "fmt" + "strconv" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + "github.com/securesign/operator/internal/controller/annotations" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/constants" + "github.com/securesign/operator/internal/controller/ctlog/utils" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const PublicKeySecretNameFormat = "ctlog-%s-pub-" + +func NewResolvePubKeyAction() action.Action[*v1alpha1.CTlog] { + return &resolvePubKeyAction{} +} + +type resolvePubKeyAction struct { + action.BaseAction +} + +func (g resolvePubKeyAction) Name() string { + return "resolve public key" +} + +func (g resolvePubKeyAction) CanHandle(ctx context.Context, instance *v1alpha1.CTlog) bool { + if instance.Status.PublicKeyRef == nil { + return true + } + + if instance.Spec.PublicKeyRef != nil && !equality.Semantic.DeepDerivative(instance.Spec.PublicKeyRef, instance.Status.PublicKeyRef) { + return true + } + + return !meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition) +} + +func (g resolvePubKeyAction) Handle(ctx context.Context, instance *v1alpha1.CTlog) *action.Result { + // Force to change PublicKeyCondition when spec has changed + if meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition) { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "resolving public key", + }) + return g.StatusUpdate(ctx, instance) + } + + var ( + err error + config *utils.SignerKey + discoveredSks *v1alpha1.SecretKeySelector + ) + + if instance.Spec.PublicKeyRef != nil { + scr := &metav1.PartialObjectMetadata{} + scr.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Secret", + }) + err = g.Client.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: instance.Spec.PublicKeyRef.Name}, scr) + if err != nil { + if k8sErrors.IsNotFound(err) { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "Waiting for secret " + instance.Spec.PublicKeyRef.Name, + }) + g.StatusUpdate(ctx, instance) + return g.Requeue() + } + return g.Failed(fmt.Errorf("ResolvePubKey: %w", err)) + } + // Add ctfe.pub label to secret + if err = constants.AddLabel(ctx, scr, g.Client, CTLPubLabel, instance.Spec.PublicKeyRef.Key); err != nil { + return g.Failed(fmt.Errorf("ResolvePubKey: %w", err)) + } + } + + config, err = utils.ResolveSignerConfig(g.Client, instance) + if err != nil { + switch { + case errors.Is(err, utils.ErrResolvePrivateKey): + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "Waiting for secret " + instance.Status.PrivateKeyRef.Name, + }) + g.StatusUpdate(ctx, instance) + return g.Requeue() + case errors.Is(err, utils.ErrResolvePrivateKeyPassword): + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "Waiting for secret " + instance.Status.PrivateKeyPasswordRef.Name, + }) + g.StatusUpdate(ctx, instance) + return g.Requeue() + default: + return g.Failed(fmt.Errorf("%w: %w", ErrParseSignerKey, err)) + } + } + + publicKey, err := config.PublicKeyPEM() + if err != nil { + return g.Failed(fmt.Errorf("%w: %w", ErrGenerateSignerKey, err)) + } + + scrl := &metav1.PartialObjectMetadataList{} + scrl.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Secret", + }) + + if err = k8sutils.FindByLabelSelector(ctx, g.Client, scrl, instance.Namespace, CTLPubLabel); err != nil { + return g.Failed(fmt.Errorf("ResolvePubKey: find secrets failed: %w", err)) + } + + // Search if exists a secret with rhtas.redhat.com/ctfe.pub label + for _, secret := range scrl.Items { + sks := v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{Name: secret.Name}, + Key: secret.Labels[CTLPubLabel], + } + + // Compare key from API and from discovered secret + var sksPublicKey utils.PEM + sksPublicKey, err = k8sutils.GetSecretData(g.Client, instance.Namespace, &sks) + if err != nil { + return g.Failed(fmt.Errorf("ResolvePubKey: failed to read `%s` secret's data: %w", sks.Name, err)) + } + + if bytes.Equal(sksPublicKey, publicKey) { + discoveredSks = &sks + continue + } + + // Remove label from secret + if err = constants.RemoveLabel(ctx, &secret, g.Client, CTLPubLabel); err != nil { + return g.Failed(fmt.Errorf("ResolvePubKey: %w", err)) + } + + message := fmt.Sprintf("Removed '%s' label from %s secret", CTLPubLabel, secret.Name) + g.Recorder.Event(instance, v1.EventTypeNormal, "PublicKeySecretLabelRemoved", message) + g.Logger.Info(message) + } + + if discoveredSks == nil { + // Create new secret with public key + const keyName = "public" + labels := constants.LabelsFor(ComponentName, DeploymentName, instance.Name) + labels[CTLPubLabel] = keyName + + newConfig := k8sutils.CreateImmutableSecret( + fmt.Sprintf(PublicKeySecretNameFormat, instance.Name), + instance.Namespace, + map[string][]byte{ + keyName: publicKey, + }, + labels) + + if newConfig.Annotations == nil { + newConfig.Annotations = make(map[string]string) + } + newConfig.Annotations[annotations.TreeId] = strconv.FormatInt(ptr.Deref(instance.Status.TreeID, 0), 10) + + if err = g.Client.Create(ctx, newConfig); err != nil { + return g.FailedWithStatusUpdate(ctx, err, instance) + } + + g.Recorder.Eventf(instance, v1.EventTypeNormal, "PublicKeySecretCreated", "New CTlog public key created: %s", newConfig.Name) + instance.Status.PublicKeyRef = &v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{Name: newConfig.Name}, + Key: keyName, + } + } else { + instance.Status.PublicKeyRef = discoveredSks + g.Recorder.Eventf(instance, v1.EventTypeNormal, "PublicKeySecretDiscovered", "Existing public key discovered: %s", discoveredSks.Name) + } + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }) + return g.StatusUpdate(ctx, instance) +} diff --git a/internal/controller/ctlog/actions/resolve_pub_key_test.go b/internal/controller/ctlog/actions/resolve_pub_key_test.go new file mode 100644 index 000000000..b220d3f88 --- /dev/null +++ b/internal/controller/ctlog/actions/resolve_pub_key_test.go @@ -0,0 +1,438 @@ +package actions + +import ( + "context" + _ "embed" + "reflect" + "testing" + + "github.com/onsi/gomega/gstruct" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/gomega" + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + "github.com/securesign/operator/internal/controller/constants" + testAction "github.com/securesign/operator/internal/testing/action" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestResolvePubKey_CanHandle(t *testing.T) { + tests := []struct { + name string + status []metav1.Condition + canHandle bool + publicKeyRef *rhtasv1alpha1.SecretKeySelector + statusPublicKeyRef *rhtasv1alpha1.SecretKeySelector + }{ + { + name: "spec.publicKeyRef is not nil and status.publicKeyRef is nil", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + publicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + statusPublicKeyRef: nil, + }, + { + name: "spec.publicKeyRef is nil and status.publicKeyRef is not nil", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: false, + publicKeyRef: nil, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + }, + { + name: "spec.publicKeyRef is nil and status.publicKeyRef is nil", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + publicKeyRef: nil, + statusPublicKeyRef: nil, + }, + { + name: "spec.publicKeyRef != status.publicKeyRef", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, + publicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "new_secret"}, Key: "public"}, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old_secret"}, Key: "public"}, + }, + { + name: "spec.publicKeyRef == status.publicKeyRef", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: false, + publicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + }, + { + name: "no phase condition", + status: []metav1.Condition{}, + canHandle: true, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + }, + { + name: "ConditionFalse", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "treeID changed", + }, + }, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + canHandle: true, + }, + { + name: "ConditionTrue", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + canHandle: false, + }, + { + name: "ConditionUnknown", + status: []metav1.Condition{ + { + Type: PublicKeyCondition, + Status: metav1.ConditionUnknown, + Reason: constants.Ready, + }, + }, + statusPublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "public"}, + canHandle: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := testAction.FakeClientBuilder().Build() + a := testAction.PrepareAction(c, NewResolvePubKeyAction()) + instance := rhtasv1alpha1.CTlog{ + Spec: rhtasv1alpha1.CTlogSpec{ + PublicKeyRef: tt.publicKeyRef, + }, + Status: rhtasv1alpha1.CTlogStatus{ + PublicKeyRef: tt.statusPublicKeyRef, + }, + } + for _, status := range tt.status { + meta.SetStatusCondition(&instance.Status.Conditions, status) + } + + if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { + t.Errorf("CanHandle() = %v, want %v", got, tt.canHandle) + } + }) + } +} + +func TestResolvePubKey_Handle(t *testing.T) { + g := NewWithT(t) + type env struct { + spec rhtasv1alpha1.CTlogSpec + status rhtasv1alpha1.CTlogStatus + objects []client.Object + } + type want struct { + result *action.Result + verify func(Gomega, *rhtasv1alpha1.CTlog) + } + tests := []struct { + name string + env env + want want + }{ + { + name: "use spec.publicKeyRef", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{ + PublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "pub-secret"}, Key: "public"}, + }, + status: rhtasv1alpha1.CTlogStatus{ + PublicKeyRef: nil, + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "key-secret"}, Key: "private"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("key-secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + kubernetes.CreateSecret("pub-secret", "default", map[string][]byte{ + "public": publicKey, + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PublicKeyRef.Name).Should(Equal("pub-secret")) + g.Expect(instance.Status.PublicKeyRef.Key).Should(Equal("public")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "use spec.publicKeyRef with ctfe.pub label", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{ + PublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "pub-secret"}, Key: "public"}, + }, + status: rhtasv1alpha1.CTlogStatus{ + PublicKeyRef: nil, + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "key-secret"}, Key: "private"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("key-secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + kubernetes.CreateSecret("pub-secret", "default", map[string][]byte{ + "public": publicKey, + }, map[string]string{ + CTLPubLabel: "public", + }), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PublicKeyRef.Name).Should(Equal("pub-secret")) + g.Expect(instance.Status.PublicKeyRef.Key).Should(Equal("public")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "generate secret from private key", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{}, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + PublicKeyRef: nil, + }, + objects: []client.Object{ + kubernetes.CreateSecret("secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PublicKeyRef.Name).Should(ContainSubstring("ctlog-ctlog-pub-")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "replace publicKeyRef from spec", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{ + PublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "new_secret"}, Key: "public"}, + }, + status: rhtasv1alpha1.CTlogStatus{ + PublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old_secret"}, Key: "public"}, + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "key-secret"}, Key: "private"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("key-secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + kubernetes.CreateSecret("new_secret", "default", map[string][]byte{ + "public": publicKey, + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PublicKeyRef.Name).Should(Equal("new_secret")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "Waiting for Private Key", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{}, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "not-existing"}, Key: "private"}, + }, + objects: []client.Object{}, + }, + want: want{ + result: testAction.Requeue(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).Should(BeNil()) + g.Expect(instance.Status.Conditions).To(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Message": ContainSubstring("Waiting for secret not-existing"), + }))) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeFalse()) + }, + }, + }, + { + name: "Waiting for private key password", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{}, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + PrivateKeyPasswordRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "not-existing"}, Key: "password"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + }, + }, + want: want{ + result: testAction.Requeue(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).Should(BeNil()) + g.Expect(instance.Status.Conditions).To(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Message": ContainSubstring("Waiting for secret not-existing"), + }))) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeFalse()) + }, + }, + }, + { + name: "remove label from old secret", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{}, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + PublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old-secret"}, Key: "public"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + kubernetes.CreateSecret("old-secret", "default", map[string][]byte{ + "public": []byte("old public key data"), + }, map[string]string{ + CTLPubLabel: "public", + }), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PublicKeyRef.Name).Should(ContainSubstring("ctlog-ctlog-pub-")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + { + name: "use existing secret", + env: env{ + spec: rhtasv1alpha1.CTlogSpec{}, + status: rhtasv1alpha1.CTlogStatus{ + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "secret"}, Key: "private"}, + PublicKeyRef: &rhtasv1alpha1.SecretKeySelector{LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: "old-secret"}, Key: "public"}, + }, + objects: []client.Object{ + kubernetes.CreateSecret("secret", "default", map[string][]byte{ + "private": privateKey, + }, map[string]string{}), + kubernetes.CreateSecret("existing-secret", "default", map[string][]byte{ + "public": publicKey, + }, map[string]string{ + CTLPubLabel: "public", + }), + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { + g.Expect(instance.Status.PublicKeyRef).ShouldNot(BeNil()) + g.Expect(instance.Status.PublicKeyRef.Name).Should(ContainSubstring("existing-secret")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, PublicKeyCondition)).Should(BeTrue()) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + instance := &rhtasv1alpha1.CTlog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ctlog", + Namespace: "default", + }, + Spec: tt.env.spec, + Status: tt.env.status, + } + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Reason: constants.Pending, + }) + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: PublicKeyCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + }) + + c := testAction.FakeClientBuilder(). + WithObjects(instance). + WithStatusSubresource(instance). + WithObjects(tt.env.objects...). + Build() + + a := testAction.PrepareAction(c, NewResolvePubKeyAction()) + + if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { + t.Errorf("CanHandle() = %v, want %v", got, tt.want.result) + } + if tt.want.verify != nil { + tt.want.verify(g, instance) + } + }) + } +} diff --git a/internal/controller/ctlog/actions/resolve_tree.go b/internal/controller/ctlog/actions/resolve_tree.go index 0c885551a..1bb27850a 100644 --- a/internal/controller/ctlog/actions/resolve_tree.go +++ b/internal/controller/ctlog/actions/resolve_tree.go @@ -40,61 +40,75 @@ func (i resolveTreeAction) Name() string { } func (i resolveTreeAction) CanHandle(_ context.Context, instance *rhtasv1alpha1.CTlog) bool { - c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) - switch { - case c == nil: - return false - case c.Reason != constants.Creating && c.Reason != constants.Ready: - return false - case instance.Status.TreeID == nil: + + if instance.Status.TreeID == nil { return true - case instance.Spec.TreeID != nil: - return !equality.Semantic.DeepEqual(instance.Spec.TreeID, instance.Status.TreeID) - default: - return false } -} -func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) *action.Result { - if instance.Spec.TreeID != nil && *instance.Spec.TreeID != int64(0) { - instance.Status.TreeID = instance.Spec.TreeID - return i.StatusUpdate(ctx, instance) - } - var err error - var tree *trillian.Tree - var trillUrl string - - switch { - case instance.Spec.Trillian.Port == nil: - err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) - case instance.Spec.Trillian.Address == "": - trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.Namespace, *instance.Spec.Trillian.Port) - default: - trillUrl = fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port) - } - if err != nil { - return i.Failed(err) + if instance.Spec.TreeID != nil && !equality.Semantic.DeepEqual(instance.Spec.TreeID, instance.Status.TreeID) { + return true } - i.Logger.V(1).Info("trillian logserver", "address", trillUrl) - tree, err = i.createTree(ctx, "ctlog-tree", trillUrl, constants.CreateTreeDeadline) - if err != nil { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: ServerCondition, - Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), - }) + return !meta.IsStatusConditionTrue(instance.Status.Conditions, TreeCondition) +} + +func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) *action.Result { + // Change status ofTreeCondition when treeID modified + if meta.IsStatusConditionTrue(instance.Status.Conditions, TreeCondition) { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, + Type: TreeCondition, Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), + Reason: constants.Pending, + Message: "resolving treeID", }) - return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) + return i.StatusUpdate(ctx, instance) + } + + if instance.Spec.TreeID != nil && *instance.Spec.TreeID != int64(0) { + instance.Status.TreeID = instance.Spec.TreeID + } else { + var err error + var tree *trillian.Tree + var trillUrl string + + switch { + case instance.Spec.Trillian.Port == nil: + err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) + case instance.Spec.Trillian.Address == "": + trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.Namespace, *instance.Spec.Trillian.Port) + default: + trillUrl = fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port) + } + if err != nil { + return i.Failed(err) + } + i.Logger.V(1).Info("trillian logserver", "address", trillUrl) + + tree, err = i.createTree(ctx, "ctlog-tree", trillUrl, constants.CreateTreeDeadline) + if err != nil { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: TreeCondition, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) + } + i.Recorder.Eventf(instance, v1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", tree.TreeId) + instance.Status.TreeID = &tree.TreeId } - i.Recorder.Eventf(instance, v1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", tree.TreeId) - instance.Status.TreeID = &tree.TreeId + // invalidate server config + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: ServerConfigCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "treeID changed", + }) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: TreeCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }) return i.StatusUpdate(ctx, instance) } diff --git a/internal/controller/ctlog/actions/resolve_tree_test.go b/internal/controller/ctlog/actions/resolve_tree_test.go index 9b0bd684b..396293a0e 100644 --- a/internal/controller/ctlog/actions/resolve_tree_test.go +++ b/internal/controller/ctlog/actions/resolve_tree_test.go @@ -24,64 +24,112 @@ import ( func TestResolveTree_CanHandle(t *testing.T) { tests := []struct { name string - phase string + status []metav1.Condition canHandle bool treeID *int64 statusTreeID *int64 }{ { - name: "spec.treeID is not nil and status.treeID is nil", - phase: constants.Creating, + name: "spec.treeID is not nil and status.treeID is nil", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: true, treeID: ptr.To(int64(123456)), }, { - name: "spec.treeID != status.treeID", - phase: constants.Creating, + name: "spec.treeID != status.treeID", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: true, treeID: ptr.To(int64(123456)), statusTreeID: ptr.To(int64(654321)), }, { - name: "spec.treeID is nil and status.treeID is not nil", - phase: constants.Creating, + name: "spec.treeID is nil and status.treeID is not nil", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: false, statusTreeID: ptr.To(int64(654321)), }, { - name: "spec.treeID is nil and status.treeID is nil", - phase: constants.Creating, - canHandle: true, - }, - { - name: "no phase condition", - phase: "", - canHandle: false, + name: "status.treeID is not nil and TreeCondition is false", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionFalse, + Reason: constants.Ready, + }, + }, + canHandle: true, + statusTreeID: ptr.To(int64(654321)), }, { - name: constants.Ready, - phase: constants.Ready, + name: "spec.treeID is nil and status.treeID is nil", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: true, }, { - name: constants.Pending, - phase: constants.Pending, - canHandle: false, + name: "no phase condition", + status: []metav1.Condition{}, + canHandle: true, + statusTreeID: ptr.To(int64(654321)), }, { - name: constants.Creating, - phase: constants.Creating, - canHandle: true, + name: "ConditionFalse", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + }, + }, + statusTreeID: ptr.To(int64(654321)), + canHandle: true, }, { - name: constants.Initialize, - phase: constants.Initialize, - canHandle: false, + name: "ConditionUnknown", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionUnknown, + Reason: constants.Pending, + }, + }, + statusTreeID: ptr.To(int64(654321)), + canHandle: true, }, { - name: constants.Failure, - phase: constants.Failure, - canHandle: false, + name: "ConditionTrue", + status: []metav1.Condition{ + { + Type: TreeCondition, + Status: metav1.ConditionTrue, + Reason: constants.Pending, + }, + }, + statusTreeID: ptr.To(int64(654321)), + canHandle: false, }, } for _, tt := range tests { @@ -96,11 +144,9 @@ func TestResolveTree_CanHandle(t *testing.T) { TreeID: tt.statusTreeID, }, } - if tt.phase != "" { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Reason: tt.phase, - }) + + for _, status := range tt.status { + meta.SetStatusCondition(&instance.Status.Conditions, status) } if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { @@ -142,6 +188,9 @@ func TestResolveTree_Handle(t *testing.T) { g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically(">", 0))) g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically("==", 5555555))) + + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, ServerConfigCondition)).Should(BeFalse()) + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, TreeCondition)).Should(BeTrue()) }, }, }, @@ -161,6 +210,9 @@ func TestResolveTree_Handle(t *testing.T) { g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically("==", *ctlog.Status.TreeID))) + + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, ServerConfigCondition)).Should(BeFalse()) + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, TreeCondition)).Should(BeTrue()) }, }, }, @@ -180,6 +232,9 @@ func TestResolveTree_Handle(t *testing.T) { g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically("==", *ctlog.Status.TreeID))) g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically("==", 123456))) + + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, ServerConfigCondition)).Should(BeFalse()) + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, TreeCondition)).Should(BeTrue()) }, }, }, @@ -197,6 +252,9 @@ func TestResolveTree_Handle(t *testing.T) { verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { g.Expect(ctlog.Spec.TreeID).Should(BeNil()) g.Expect(ctlog.Status.TreeID).Should(BeNil()) + + g.Expect(meta.FindStatusCondition(ctlog.Status.Conditions, ServerConfigCondition)).Should(BeNil()) + g.Expect(meta.IsStatusConditionTrue(ctlog.Status.Conditions, TreeCondition)).Should(BeFalse()) }, }, }, @@ -254,7 +312,12 @@ func TestResolveTree_Handle(t *testing.T) { Conditions: []metav1.Condition{ { Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, + }, + { + Type: TreeCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, }, }, }, diff --git a/internal/controller/ctlog/actions/server_config.go b/internal/controller/ctlog/actions/server_config.go index 384102e1e..85081d120 100644 --- a/internal/controller/ctlog/actions/server_config.go +++ b/internal/controller/ctlog/actions/server_config.go @@ -12,11 +12,14 @@ import ( trillian "github.com/securesign/operator/internal/controller/trillian/actions" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +const ConfigSecretNameFormat = "ctlog-config-%s" + func NewServerConfigAction() action.Action[*rhtasv1alpha1.CTlog] { return &serverConfig{} } @@ -30,32 +33,40 @@ func (i serverConfig) Name() string { } func (i serverConfig) CanHandle(_ context.Context, instance *rhtasv1alpha1.CTlog) bool { - c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) - switch { - case c == nil: - return false - case c.Reason != constants.Creating && c.Reason != constants.Ready: - return false - case instance.Status.ServerConfigRef == nil: + if instance.Status.ServerConfigRef == nil { return true - case instance.Spec.ServerConfigRef != nil: + } + + if instance.Spec.ServerConfigRef != nil { return !equality.Semantic.DeepEqual(instance.Spec.ServerConfigRef, instance.Status.ServerConfigRef) - default: - return false } + + return !meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition) } func (i serverConfig) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) *action.Result { + // Return to pending state due changes in ServerConfigRef + if meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition) { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: ServerConfigCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "resolving server config", + }) + return i.StatusUpdate(ctx, instance) + } + var ( + cfg *ctlogUtils.Config err error ) if instance.Spec.ServerConfigRef != nil { instance.Status.ServerConfigRef = instance.Spec.ServerConfigRef - i.Recorder.Event(instance, corev1.EventTypeNormal, "CTLogConfigUpdated", "CTLog config updated") - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: constants.Ready, - Status: metav1.ConditionFalse, Reason: constants.Creating, Message: "CTLog config updated"}) + i.Recorder.Eventf(instance, corev1.EventTypeNormal, "CTLogConfigUpdated", "CTLog config updated: %s", instance.Status.ServerConfigRef.Name) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: ServerConfigCondition, + Status: metav1.ConditionTrue, Reason: constants.Ready, Message: "CTLog config updated"}) return i.StatusUpdate(ctx, instance) } @@ -77,31 +88,30 @@ func (i serverConfig) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) rootCerts, err := i.handleRootCertificates(instance) if err != nil { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, + Type: ServerConfigCondition, Status: metav1.ConditionFalse, - Reason: constants.Creating, + Reason: constants.Pending, Message: fmt.Sprintf("Waiting for Fulcio root certificate: %v", err.Error()), }) i.StatusUpdate(ctx, instance) return i.Requeue() } - certConfig, err := i.handlePrivateKey(instance) + signerConfig, err := ctlogUtils.ResolveSignerConfig(i.Client, instance) if err != nil { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, + Type: ServerConfigCondition, Status: metav1.ConditionFalse, - Reason: constants.Creating, + Reason: constants.Pending, Message: "Waiting for Ctlog private key secret", }) i.StatusUpdate(ctx, instance) return i.Requeue() } - var cfg map[string][]byte - if cfg, err = ctlogUtils.CreateCtlogConfig(fmt.Sprintf("%s:%d", trillianService.Address, *trillianService.Port), *instance.Status.TreeID, rootCerts, certConfig); err != nil { + if cfg, err = ctlogUtils.CTlogConfig(fmt.Sprintf("%s:%d", trillianService.Address, *trillianService.Port), *instance.Status.TreeID, rootCerts, signerConfig); err != nil { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, + Type: ServerConfigCondition, Status: metav1.ConditionFalse, Reason: constants.Failure, Message: err.Error(), @@ -109,7 +119,12 @@ func (i serverConfig) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create CTLog configuration: %w", err), instance) } - newConfig := utils.CreateImmutableSecret(fmt.Sprintf("ctlog-config-%s", instance.Name), instance.Namespace, cfg, labels) + data, err := cfg.Marshal() + if err != nil { + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("failed to marshal CTLog configuration: %w", err), instance) + } + + newConfig := utils.CreateImmutableSecret(fmt.Sprintf(ConfigSecretNameFormat, instance.Name), instance.Namespace, data, labels) if err = controllerutil.SetControllerReference(instance, newConfig, i.Client.Scheme()); err != nil { return i.Failed(fmt.Errorf("could not set controller reference for Secret: %w", err)) @@ -118,7 +133,7 @@ func (i serverConfig) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) _, err = i.Ensure(ctx, newConfig) if err != nil { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, + Type: ServerConfigCondition, Status: metav1.ConditionFalse, Reason: constants.Failure, Message: err.Error(), @@ -126,38 +141,29 @@ func (i serverConfig) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) return i.FailedWithStatusUpdate(ctx, err, instance) } + // invalidate server config + if instance.Status.ServerConfigRef != nil { + if err = i.Client.Delete(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: instance.Status.ServerConfigRef.Name, + Namespace: instance.Namespace, + }, + }); err != nil { + if !k8sErrors.IsNotFound(err) { + return i.Failed(err) + } + } + i.Recorder.Eventf(instance, corev1.EventTypeNormal, "CTLogConfigDeleted", "CTLog config deleted: %s", instance.Status.ServerConfigRef.Name) + } + instance.Status.ServerConfigRef = &rhtasv1alpha1.LocalObjectReference{Name: newConfig.Name} - i.Recorder.Event(instance, corev1.EventTypeNormal, "CTLogConfigUpdated", "CTLog config updated") - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: constants.Ready, - Status: metav1.ConditionFalse, Reason: constants.Creating, Message: "Server config created"}) + i.Recorder.Eventf(instance, corev1.EventTypeNormal, "CTLogConfigUpdated", "CTLog config updated: %s", newConfig.Name) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: ServerConfigCondition, + Status: metav1.ConditionTrue, Reason: constants.Ready, Message: "CTLog config created"}) return i.StatusUpdate(ctx, instance) } -func (i serverConfig) handlePrivateKey(instance *rhtasv1alpha1.CTlog) (*ctlogUtils.PrivateKeyConfig, error) { - if instance == nil { - return nil, nil - } - private, err := utils.GetSecretData(i.Client, instance.Namespace, instance.Status.PrivateKeyRef) - if err != nil { - return nil, err - } - public, err := utils.GetSecretData(i.Client, instance.Namespace, instance.Status.PublicKeyRef) - if err != nil { - return nil, err - } - password, err := utils.GetSecretData(i.Client, instance.Namespace, instance.Status.PrivateKeyPasswordRef) - if err != nil { - return nil, err - } - - return &ctlogUtils.PrivateKeyConfig{ - PrivateKey: private, - PublicKey: public, - PrivateKeyPass: password, - }, nil -} - func (i serverConfig) handleRootCertificates(instance *rhtasv1alpha1.CTlog) ([]ctlogUtils.RootCertificate, error) { certs := make([]ctlogUtils.RootCertificate, 0) diff --git a/internal/controller/ctlog/actions/server_config_test.go b/internal/controller/ctlog/actions/server_config_test.go index a9a30c10d..9906f362c 100644 --- a/internal/controller/ctlog/actions/server_config_test.go +++ b/internal/controller/ctlog/actions/server_config_test.go @@ -6,6 +6,8 @@ import ( "reflect" "testing" + "github.com/securesign/operator/internal/controller/ctlog/utils" + "github.com/onsi/gomega/gstruct" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,85 +25,115 @@ import ( var ( //go:embed testdata/private_key.pem - privateKey []byte + privateKey utils.PEM + //go:embed testdata/private_key_pass.pem + privatePassKey utils.PEM //go:embed testdata/public_key.pem - publicKey []byte + publicKey utils.PEM //go:embed testdata/cert.pem - cert []byte + cert utils.PEM ) func TestServerConfig_CanHandle(t *testing.T) { tests := []struct { name string - phase string + status []metav1.Condition canHandle bool serverConfigRef *rhtasv1alpha1.LocalObjectReference statusServerConfigRef *rhtasv1alpha1.LocalObjectReference }{ { - name: "spec.serverConfigRef is not nil and status.serverConfigRef is nil", - phase: constants.Creating, + name: "spec.serverConfigRef is not nil and status.serverConfigRef is nil", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: true, serverConfigRef: &rhtasv1alpha1.LocalObjectReference{Name: "config"}, statusServerConfigRef: nil, }, { - name: "spec.serverConfigRef is nil and status.serverConfigRef is not nil", - phase: constants.Creating, + name: "spec.serverConfigRef is nil and status.serverConfigRef is not nil", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: false, serverConfigRef: nil, statusServerConfigRef: &rhtasv1alpha1.LocalObjectReference{Name: "config"}, }, { - name: "spec.serverConfigRef is nil and status.serverConfigRef is nil", - phase: constants.Creating, + name: "spec.serverConfigRef is nil and status.serverConfigRef is nil", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: true, serverConfigRef: nil, statusServerConfigRef: nil, }, { - name: "spec.serverConfigRef != status.serverConfigRef", - phase: constants.Creating, + name: "spec.serverConfigRef != status.serverConfigRef", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: true, serverConfigRef: &rhtasv1alpha1.LocalObjectReference{Name: "new_config"}, statusServerConfigRef: &rhtasv1alpha1.LocalObjectReference{Name: "old_config"}, }, { - name: "spec.serverConfigRef == status.serverConfigRef", - phase: constants.Creating, + name: "spec.serverConfigRef == status.serverConfigRef", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, canHandle: false, serverConfigRef: &rhtasv1alpha1.LocalObjectReference{Name: "config"}, statusServerConfigRef: &rhtasv1alpha1.LocalObjectReference{Name: "config"}, }, { name: "no phase condition", - phase: "", - canHandle: false, - }, - { - name: constants.Ready, - phase: constants.Ready, + status: []metav1.Condition{}, canHandle: true, }, { - name: constants.Pending, - phase: constants.Pending, - canHandle: false, - }, - { - name: constants.Creating, - phase: constants.Creating, + name: "ServerConfigCondition == false", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, + Message: "treeID changed", + }, + }, canHandle: true, }, { - name: constants.Initialize, - phase: constants.Initialize, - canHandle: false, - }, - { - name: constants.Failure, - phase: constants.Failure, - canHandle: false, + name: "ServerConfigCondition == true", + status: []metav1.Condition{ + { + Type: ServerConfigCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }, + }, + canHandle: true, }, } for _, tt := range tests { @@ -116,11 +148,8 @@ func TestServerConfig_CanHandle(t *testing.T) { ServerConfigRef: tt.statusServerConfigRef, }, } - if tt.phase != "" { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Reason: tt.phase, - }) + for _, status := range tt.status { + meta.SetStatusCondition(&instance.Status.Conditions, status) } if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { @@ -161,6 +190,8 @@ func TestServerConfig_Handle(t *testing.T) { verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { g.Expect(instance.Status.ServerConfigRef).ShouldNot(BeNil()) g.Expect(instance.Status.ServerConfigRef.Name).Should(Equal("config")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) }, }, }, @@ -193,6 +224,8 @@ func TestServerConfig_Handle(t *testing.T) { verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { g.Expect(instance.Status.ServerConfigRef).ShouldNot(BeNil()) g.Expect(instance.Status.ServerConfigRef.Name).Should(ContainSubstring("ctlog-config-")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) }, }, }, @@ -211,6 +244,8 @@ func TestServerConfig_Handle(t *testing.T) { verify: func(g Gomega, instance *rhtasv1alpha1.CTlog) { g.Expect(instance.Status.ServerConfigRef).ShouldNot(BeNil()) g.Expect(instance.Status.ServerConfigRef.Name).Should(Equal("new_config")) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition)).Should(BeTrue()) }, }, }, @@ -246,6 +281,8 @@ func TestServerConfig_Handle(t *testing.T) { g.Expect(instance.Status.Conditions).To(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ "Message": ContainSubstring("Waiting for Fulcio root certificate: not-existing/cert"), }))) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition)).Should(BeFalse()) }, }, }, @@ -280,6 +317,8 @@ func TestServerConfig_Handle(t *testing.T) { g.Expect(instance.Status.Conditions).To(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ "Message": ContainSubstring("Waiting for Ctlog private key secret"), }))) + + g.Expect(meta.IsStatusConditionTrue(instance.Status.Conditions, ServerConfigCondition)).Should(BeFalse()) }, }, }, @@ -298,7 +337,13 @@ func TestServerConfig_Handle(t *testing.T) { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: constants.Ready, - Reason: constants.Creating, + Reason: constants.Pending, + }) + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: ServerConfigCondition, + Status: metav1.ConditionFalse, + Reason: constants.Pending, }) c := testAction.FakeClientBuilder(). diff --git a/internal/controller/ctlog/actions/testdata/private_key_pass.pem b/internal/controller/ctlog/actions/testdata/private_key_pass.pem new file mode 100644 index 000000000..84d1ea22e --- /dev/null +++ b/internal/controller/ctlog/actions/testdata/private_key_pass.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,CD46D8406445C428F69BB8BBE50480E2 + +FTPDStHxGzG2XIoPVo0WIbDzy/3vJW0Va7hodkU9V3DHgo0cqRPXPW4qlkQjjRbJ +p9hn+pceMHbNuWOK/iCGKTW9OMWeJqLs0N8WE/xp9GpH9AnrtLD2Jfa9XowHIHU3 +beliLg+MxjnqgbMPD0o5jqvlr8j33zRqjlqaiJcH8w8= +-----END EC PRIVATE KEY----- diff --git a/internal/controller/ctlog/ctlog_controller.go b/internal/controller/ctlog/ctlog_controller.go index f4a7052fc..4a134b5ae 100644 --- a/internal/controller/ctlog/ctlog_controller.go +++ b/internal/controller/ctlog/ctlog_controller.go @@ -89,15 +89,23 @@ func (r *CTlogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl target := instance.DeepCopy() acs := []action.Action[*rhtasv1alpha1.CTlog]{ transitions.NewToPendingPhaseAction[*rhtasv1alpha1.CTlog](func(_ *rhtasv1alpha1.CTlog) []string { - return []string{actions.CertCondition} + return []string{ + actions.CertCondition, + actions.SignerCondition, + actions.ServerConfigCondition, + actions.PublicKeyCondition, + actions.TreeCondition, + } }), - transitions.NewToCreatePhaseAction[*rhtasv1alpha1.CTlog](), + actions.NewGenerateSignerAction(), actions.NewHandleFulcioCertAction(), - actions.NewHandleKeysAction(), + actions.NewResolvePubKeyAction(), actions.NewResolveTreeAction(), actions.NewServerConfigAction(), + transitions.NewToCreatePhaseAction[*rhtasv1alpha1.CTlog](), + actions.NewRBACAction(), actions.NewDeployAction(), actions.NewServiceAction(), diff --git a/internal/controller/ctlog/ctlog_controller_test.go b/internal/controller/ctlog/ctlog_controller_test.go index 54d60daa7..fde6699fd 100644 --- a/internal/controller/ctlog/ctlog_controller_test.go +++ b/internal/controller/ctlog/ctlog_controller_test.go @@ -25,7 +25,6 @@ import ( "github.com/securesign/operator/internal/controller/constants" "github.com/securesign/operator/internal/controller/ctlog/actions" fulcio "github.com/securesign/operator/internal/controller/fulcio/actions" - trillian "github.com/securesign/operator/internal/controller/trillian/actions" k8sTest "github.com/securesign/operator/internal/testing/kubernetes" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -116,14 +115,6 @@ var _ = Describe("CTlog controller", func() { return meta.IsStatusConditionPresentAndEqual(found.Status.Conditions, constants.Ready, metav1.ConditionFalse) }).Should(BeTrue()) - By("Creating trillian service") - Expect(k8sClient.Create(ctx, kubernetes.CreateService(Namespace, trillian.LogserverDeploymentName, trillian.ServerPortName, trillian.ServerPort, trillian.ServerPort, constants.LabelsForComponent(trillian.LogServerComponentName, instance.Name)))).To(Succeed()) - Eventually(func(g Gomega) string { - found := &v1alpha1.CTlog{} - g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) - return meta.FindStatusCondition(found.Status.Conditions, constants.Ready).Reason - }).Should(Equal(constants.Creating)) - By("Creating fulcio root cert") Expect(k8sClient.Create(ctx, kubernetes.CreateSecret("test", Namespace, map[string][]byte{"cert": []byte("fakeCert")}, diff --git a/internal/controller/ctlog/ctlog_hot_update_test.go b/internal/controller/ctlog/ctlog_hot_update_test.go index be59556e5..a55da2b36 100644 --- a/internal/controller/ctlog/ctlog_hot_update_test.go +++ b/internal/controller/ctlog/ctlog_hot_update_test.go @@ -29,7 +29,7 @@ import ( "github.com/securesign/operator/internal/controller/constants" "github.com/securesign/operator/internal/controller/ctlog/actions" fulcio "github.com/securesign/operator/internal/controller/fulcio/actions" - trillian "github.com/securesign/operator/internal/controller/trillian/actions" + testErrors "github.com/securesign/operator/internal/testing/errors" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -42,165 +42,200 @@ import ( "k8s.io/apimachinery/pkg/types" ) -var _ = Describe("CTlog update test", func() { - Context("CTlog update test", func() { +var _ = Describe("CTlog update test", Ordered, func() { + const ( + Name = "test" + Namespace = "update" + ) - const ( - Name = "test" - Namespace = "update" - ) + ctx := context.Background() - ctx := context.Background() + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: Namespace, + }, + } - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, - }, - } + typeNamespaceName := types.NamespacedName{Name: Name, Namespace: Namespace} + instance := &v1alpha1.CTlog{} + var fulcioCa *corev1.Secret - typeNamespaceName := types.NamespacedName{Name: Name, Namespace: Namespace} - instance := &v1alpha1.CTlog{} + BeforeAll(func() { + By("Creating the Namespace to perform the tests") + err := k8sClient.Create(ctx, namespace) + Expect(err).To(Not(HaveOccurred())) + }) - BeforeEach(func() { - By("Creating the Namespace to perform the tests") - err := k8sClient.Create(ctx, namespace) + AfterAll(func() { + By("removing the custom resource for the Kind CTlog") + found := &v1alpha1.CTlog{} + err := k8sClient.Get(ctx, typeNamespaceName, found) + Expect(err).To(Not(HaveOccurred())) + + Eventually(func() error { + return k8sClient.Delete(context.TODO(), found) + }, 3*time.Minute, time.Second).Should(Succeed()) + + // TODO(user): Attention if you improve this code by adding other context test you MUST + // be aware of the current delete namespace limitations. + // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations + By("Deleting the Namespace to perform the tests") + _ = k8sClient.Delete(ctx, namespace) + }) + + It("should successfully reconcile a custom resource for CTlog", func() { + By("creating the custom resource for the Kind CTlog") + err := k8sClient.Get(ctx, typeNamespaceName, instance) + if err != nil && errors.IsNotFound(err) { + // Let's mock our custom resource at the same way that we would + // apply on the cluster the manifest under config/samples + ptr := int64(1) + instance := &v1alpha1.CTlog{ + ObjectMeta: metav1.ObjectMeta{ + Name: Name, + Namespace: Namespace, + }, + + Spec: v1alpha1.CTlogSpec{ + TreeID: &ptr, + }, + } + err = k8sClient.Create(ctx, instance) Expect(err).To(Not(HaveOccurred())) - }) - AfterEach(func() { - By("removing the custom resource for the Kind CTlog") + } + + By("Checking if the custom resource was successfully created") + Eventually(func() error { found := &v1alpha1.CTlog{} - err := k8sClient.Get(ctx, typeNamespaceName, found) - Expect(err).To(Not(HaveOccurred())) + return k8sClient.Get(ctx, typeNamespaceName, found) + }).Should(Succeed()) + + By("Creating fulcio root cert") + fulcioCa = kubernetes.CreateSecret("test", Namespace, + map[string][]byte{"cert": []byte("fakeCert")}, + map[string]string{fulcio.FulcioCALabel: "cert"}, + ) + Expect(k8sClient.Create(ctx, fulcioCa)).To(Succeed()) + + deployment := &appsv1.Deployment{} + By("Checking if Deployment was successfully created in the reconciliation") + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, deployment) + }).Should(Succeed()) + + By("Move to Ready phase") + // Workaround to succeed condition for Ready phase + Expect(k8sTest.SetDeploymentToReady(ctx, k8sClient, deployment)).To(Succeed()) - Eventually(func() error { - return k8sClient.Delete(context.TODO(), found) - }, 3*time.Minute, time.Second).Should(Succeed()) - - // TODO(user): Attention if you improve this code by adding other context test you MUST - // be aware of the current delete namespace limitations. - // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations - By("Deleting the Namespace to perform the tests") - _ = k8sClient.Delete(ctx, namespace) - }) - - It("should successfully reconcile a custom resource for CTlog", func() { - By("creating the custom resource for the Kind CTlog") - err := k8sClient.Get(ctx, typeNamespaceName, instance) - if err != nil && errors.IsNotFound(err) { - // Let's mock our custom resource at the same way that we would - // apply on the cluster the manifest under config/samples - ptr := int64(1) - instance := &v1alpha1.CTlog{ - ObjectMeta: metav1.ObjectMeta{ - Name: Name, - Namespace: Namespace, - }, - - Spec: v1alpha1.CTlogSpec{ - TreeID: &ptr, - }, - } - err = k8sClient.Create(ctx, instance) - Expect(err).To(Not(HaveOccurred())) + By("Waiting until CTlog instance is Ready") + Eventually(func(g Gomega) bool { + found := &v1alpha1.CTlog{} + g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) + return meta.IsStatusConditionTrue(found.Status.Conditions, constants.Ready) + }).Should(BeTrue()) + + }) + + It("change Fulcio CA", func() { + + By("get current instance") + oldInstance := &v1alpha1.CTlog{} + oldDeployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, typeNamespaceName, oldInstance)).Should(Succeed()) + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, oldDeployment)).Should(Succeed()) + + By("change Fulcio CA") + Expect(k8sClient.Delete(ctx, fulcioCa)).To(Succeed()) + fulcioCa = kubernetes.CreateSecret("test2", Namespace, + map[string][]byte{"cert": []byte("fakeCert2")}, + map[string]string{fulcio.FulcioCALabel: "cert"}, + ) + Expect(k8sClient.Create(ctx, fulcioCa)).To(Succeed()) + By("CA has changed in status field") + Eventually(func(g Gomega) { + found := &v1alpha1.CTlog{} + g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) + g.Expect(found.Status.RootCertificates). + Should(HaveExactElements(WithTransform(func(ks v1alpha1.SecretKeySelector) string { + return ks.Name + }, Equal("test2")))) + }).Should(Succeed()) + + By("Server config has changed") + Eventually(func(g Gomega) { + found := &v1alpha1.CTlog{} + g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) + g.Expect(found.Status.ServerConfigRef).ToNot(BeNil()) + g.Expect(found.Status.ServerConfigRef.Name).ShouldNot(Equal(oldInstance.Status.ServerConfigRef.Name)) + }).Should(Succeed()) + + By("CTL deployment is updated") + Eventually(func() bool { + updated := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, updated)).To(Succeed()) + return equality.Semantic.DeepDerivative(oldDeployment.Spec.Template.Spec.Volumes, updated.Spec.Template.Spec.Volumes) + }).Should(BeFalse()) + + By("Move to Ready phase") + current := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, current)).To(Succeed()) + Expect(k8sTest.SetDeploymentToReady(ctx, k8sClient, current)).To(Succeed()) + }) + + It("Private key has changed", func() { + + By("get current instance") + oldInstance := &v1alpha1.CTlog{} + oldDeployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, typeNamespaceName, oldInstance)).Should(Succeed()) + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, oldDeployment)).Should(Succeed()) + + By("create a new signer key") + key, err := utils.NewSignerConfig() + Expect(err).To(Not(HaveOccurred())) + Expect(k8sClient.Create(ctx, kubernetes.CreateSecret("key-secret", Namespace, + map[string][]byte{"private": testErrors.IgnoreError(key.PrivateKeyPEM())}, constants.LabelsFor(actions.ComponentName, Name, instance.Name)))).To(Succeed()) + + By("modify spec.privateKeyRef") + found := &v1alpha1.CTlog{} + Eventually(func(g Gomega) error { + g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) + found.Spec.PrivateKeyRef = &v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: "key-secret", + }, + Key: "private", } + return k8sClient.Update(ctx, found) + }).Should(Succeed()) - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &v1alpha1.CTlog{} - return k8sClient.Get(ctx, typeNamespaceName, found) - }).Should(Succeed()) - - By("Creating trillian service") - Expect(k8sClient.Create(ctx, kubernetes.CreateService(Namespace, trillian.LogserverDeploymentName, trillian.ServerPortName, trillian.ServerPort, trillian.ServerPort, constants.LabelsForComponent(trillian.LogServerComponentName, instance.Name)))).To(Succeed()) - - By("Creating fulcio root cert") - fulcioCa := kubernetes.CreateSecret("test", Namespace, - map[string][]byte{"cert": []byte("fakeCert")}, - map[string]string{fulcio.FulcioCALabel: "cert"}, - ) - Expect(k8sClient.Create(ctx, fulcioCa)).To(Succeed()) - - deployment := &appsv1.Deployment{} - By("Checking if Deployment was successfully created in the reconciliation") - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, deployment) - }).Should(Succeed()) - - By("Move to Ready phase") - // Workaround to succeed condition for Ready phase - Expect(k8sTest.SetDeploymentToReady(ctx, k8sClient, deployment)).To(Succeed()) - - By("Waiting until CTlog instance is Ready") - Eventually(func(g Gomega) bool { - found := &v1alpha1.CTlog{} - g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) - return meta.IsStatusConditionTrue(found.Status.Conditions, constants.Ready) - }).Should(BeTrue()) - - By("Fulcio CA has changed") - Expect(k8sClient.Delete(ctx, fulcioCa)).To(Succeed()) - fulcioCa = kubernetes.CreateSecret("test2", Namespace, - map[string][]byte{"cert": []byte("fakeCert2")}, - map[string]string{fulcio.FulcioCALabel: "cert"}, - ) - Expect(k8sClient.Create(ctx, fulcioCa)).To(Succeed()) - - By("CA has changed in status field") - Eventually(func(g Gomega) { - found := &v1alpha1.CTlog{} - g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) - g.Expect(found.Status.RootCertificates). - Should(HaveExactElements(WithTransform(func(ks v1alpha1.SecretKeySelector) string { - return ks.Name - }, Equal("test2")))) - }).Should(Succeed()) - - By("CTL deployment is updated") - Eventually(func() bool { - updated := &appsv1.Deployment{} - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, updated)).To(Succeed()) - return equality.Semantic.DeepDerivative(deployment.Spec.Template.Spec.Volumes, updated.Spec.Template.Spec.Volumes) - }).Should(BeFalse()) - - By("Move to Ready phase") - deployment = &appsv1.Deployment{} - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, deployment)).To(Succeed()) - Expect(k8sTest.SetDeploymentToReady(ctx, k8sClient, deployment)).To(Succeed()) - - By("Private key has changed") - key, err := utils.CreatePrivateKey() - Expect(err).To(Not(HaveOccurred())) - Expect(k8sClient.Create(ctx, kubernetes.CreateSecret("key-secret", Namespace, - map[string][]byte{"private": key.PrivateKey}, constants.LabelsFor(actions.ComponentName, Name, instance.Name)))).To(Succeed()) + By("CTLog status field changed") + Eventually(func(g Gomega) string { + found := &v1alpha1.CTlog{} + g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) + return found.Status.PrivateKeyRef.Name + }).Should(Equal("key-secret")) - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, deployment)).To(Succeed()) + By("Server config has changed") + Eventually(func(g Gomega) { found := &v1alpha1.CTlog{} - Eventually(func(g Gomega) error { - g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) - found.Spec.PrivateKeyRef = &v1alpha1.SecretKeySelector{ - LocalObjectReference: v1alpha1.LocalObjectReference{ - Name: "key-secret", - }, - Key: "private", - } - return k8sClient.Update(ctx, found) - }).Should(Succeed()) - - By("CTLog status field changed") - Eventually(func(g Gomega) string { - found := &v1alpha1.CTlog{} - g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) - return found.Status.PrivateKeyRef.Name - }).Should(Equal("key-secret")) - - By("CTL deployment is updated") - Eventually(func(g Gomega) bool { - updated := &appsv1.Deployment{} - g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, updated)).To(Succeed()) - return equality.Semantic.DeepDerivative(deployment.Spec.Template.Spec.Volumes, updated.Spec.Template.Spec.Volumes) - }).Should(BeFalse()) - }) + g.Expect(k8sClient.Get(ctx, typeNamespaceName, found)).Should(Succeed()) + g.Expect(found.Status.ServerConfigRef).ToNot(BeNil()) + g.Expect(found.Status.ServerConfigRef.Name).ShouldNot(Equal(oldInstance.Status.ServerConfigRef.Name)) + }).Should(Succeed()) + + By("CTL deployment is updated") + Eventually(func(g Gomega) bool { + updated := &appsv1.Deployment{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, updated)).To(Succeed()) + return equality.Semantic.DeepDerivative(oldDeployment.Spec.Template.Spec.Volumes, updated.Spec.Template.Spec.Volumes) + }).Should(BeFalse()) + + By("Move to Ready phase") + current := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: actions.DeploymentName, Namespace: Namespace}, current)).To(Succeed()) + Expect(k8sTest.SetDeploymentToReady(ctx, k8sClient, current)).To(Succeed()) }) }) diff --git a/internal/controller/ctlog/utils/certificates.go b/internal/controller/ctlog/utils/certificates.go deleted file mode 100644 index 22641569f..000000000 --- a/internal/controller/ctlog/utils/certificates.go +++ /dev/null @@ -1,40 +0,0 @@ -package utils - -import ( - "fmt" - "net/url" - "time" - - fulcioclient "github.com/sigstore/fulcio/pkg/api" - "github.com/sigstore/sigstore/pkg/cryptoutils" -) - -type RootCertificate []byte - -func GetFulcioRootCert(fulcioUrl string) (RootCertificate, error) { - u, err := url.Parse(fulcioUrl) - if err != nil { - return nil, fmt.Errorf("invalid Fulcio URL %s : %v", fulcioUrl, err) - } - - var root *fulcioclient.RootResponse - - for i := 0; i < 10; i++ { - client := fulcioclient.NewClient(u) - root, err = client.RootCert() - if err != nil || root == nil { - time.Sleep(time.Duration(5) * time.Second) - } - } - - if err != nil { - return nil, fmt.Errorf("failed to fetch Fulcio Root cert: %w", err) - } - - // Fetch only root certificate from the chain - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(root.ChainPEM) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal certficate chain: %w", err) - } - return cryptoutils.MarshalCertificateToPEM(certs[len(certs)-1]) -} diff --git a/internal/controller/ctlog/utils/ctlog_config.go b/internal/controller/ctlog/utils/ctlog_config.go index 5434ddbb4..e6ebf8afc 100644 --- a/internal/controller/ctlog/utils/ctlog_config.go +++ b/internal/controller/ctlog/utils/ctlog_config.go @@ -3,9 +3,6 @@ package utils import ( "bytes" "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "encoding/pem" "fmt" "github.com/google/certificate-transparency-go/trillian/ctfe/configpb" @@ -46,11 +43,9 @@ var supportedCurves = map[string]elliptic.Curve{ // technically they are not part of the config, however because we create a // secret/CM that we then mount, they need to be synced. type Config struct { - PrivKey []byte - PrivKeyPassword []byte - PubKey []byte - LogID int64 - LogPrefix string + Signer SignerKey + LogID int64 + LogPrefix string // Address of the gRPC Trillian Admin Server (host:port) TrillianServerAddr string @@ -84,7 +79,7 @@ func (c *Config) AddRootCertificate(root RootCertificate) error { // public - CTLog public key, PEM encoded // fulcio-%d - For each fulcioCerts, contains one entry so we can support // multiple. -func (c *Config) MarshalConfig() ([]byte, error) { +func (c *Config) marshalConfig() ([]byte, error) { // Since we can have multiple Fulcio secrets, we need to construct a set // of files containing them for the RootsPemFile. Names don't matter // so we just call them fulcio-% @@ -95,9 +90,9 @@ func (c *Config) MarshalConfig() ([]byte, error) { rootPems = append(rootPems, fmt.Sprintf("%sfulcio-%d", rootsPemFileDir, i)) } - block, _ := pem.Decode(c.PubKey) - if block == nil { - return nil, fmt.Errorf("failed to decode private key") + publicKey, err := c.Signer.PublicKey() + if err != nil { + return nil, err } proto := configpb.LogConfig{ @@ -106,8 +101,8 @@ func (c *Config) MarshalConfig() ([]byte, error) { RootsPemFile: rootPems, PrivateKey: mustMarshalAny(&keyspb.PEMKeyFile{ Path: privateKeyFile, - Password: string(c.PrivKeyPassword)}), - PublicKey: &keyspb.PublicKey{Der: block.Bytes}, + Password: string(c.Signer.PrivateKeyPassword())}), + PublicKey: &keyspb.PublicKey{Der: publicKey}, LogBackendName: "trillian", ExtKeyUsages: []string{"CodeSigning"}, } @@ -130,73 +125,62 @@ func (c *Config) MarshalConfig() ([]byte, error) { return marshalledConfig, nil } -func mustMarshalAny(pb proto.Message) *anypb.Any { - ret, err := anypb.New(pb) +func (c *Config) Marshal() (map[string][]byte, error) { + config, err := c.marshalConfig() if err != nil { - panic(fmt.Sprintf("MarshalAny failed: %v", err)) + return nil, fmt.Errorf("failed to marshal ctlog config: %v", err) } - return ret -} -func createConfigWithKeys(certConfig *PrivateKeyConfig) (*Config, error) { - config := &Config{ - PubKey: certConfig.PublicKey, + keyPEM, err := c.Signer.PrivateKeyPEM() + if err != nil { + return nil, fmt.Errorf("failed to marshal private key: %v", err) } - if certConfig.PrivateKeyPass != nil { - config.PrivKeyPassword = certConfig.PrivateKeyPass - config.PrivKey = certConfig.PrivateKey - } else { - // private key MUST be encrypted by password - config.PrivKeyPassword = common.GeneratePassword(8) - block, _ := pem.Decode(certConfig.PrivateKey) - if block == nil { - return nil, fmt.Errorf("failed to decode private key") - } - // Encrypt the pem - encryptedBlock, err := x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, config.PrivKeyPassword, x509.PEMCipherAES256) // nolint - if err != nil { - return nil, fmt.Errorf("failed to encrypt private key: %w", err) - } - privPEM := pem.EncodeToMemory(encryptedBlock) - if privPEM == nil { - return nil, fmt.Errorf("failed to encode encrypted private key") - } - config.PrivKey = privPEM + pubPEM, err := c.Signer.PublicKeyPEM() + if err != nil { + return nil, fmt.Errorf("failed to marshal public key: %v", err) + } + data := map[string][]byte{ + ConfigKey: config, + PrivateKey: keyPEM, + PublicKey: pubPEM, + Password: c.Signer.PrivateKeyPassword(), } - return config, nil + for i, cert := range c.RootCerts { + fulcioKey := fmt.Sprintf("fulcio-%d", i) + data[fulcioKey] = cert + } + + return data, nil } -func CreateCtlogConfig(trillianUrl string, treeID int64, rootCerts []RootCertificate, keyConfig *PrivateKeyConfig) (map[string][]byte, error) { - ctlogConfig, err := createConfigWithKeys(keyConfig) +func mustMarshalAny(pb proto.Message) *anypb.Any { + ret, err := anypb.New(pb) if err != nil { - return nil, err + panic(fmt.Sprintf("MarshalAny failed: %v", err)) } - ctlogConfig.LogID = treeID - ctlogConfig.LogPrefix = "trusted-artifact-signer" - ctlogConfig.TrillianServerAddr = trillianUrl + return ret +} - for _, cert := range rootCerts { - if err = ctlogConfig.AddRootCertificate(cert); err != nil { - return nil, fmt.Errorf("Failed to add fulcio root: %v", err) - } +func CTlogConfig(url string, treeID int64, rootCerts []RootCertificate, signer *SignerKey) (*Config, error) { + config := &Config{ + Signer: *signer, + LogID: treeID, + LogPrefix: "trusted-artifact-signer", + TrillianServerAddr: url, } - config, err := ctlogConfig.MarshalConfig() - if err != nil { - return nil, fmt.Errorf("Failed to marshal ctlog config: %v", err) + // private key MUST be encrypted by password + if len(config.Signer.PrivateKeyPassword()) == 0 { + config.Signer.password = common.GeneratePassword(20) } - data := map[string][]byte{ - ConfigKey: config, - PrivateKey: ctlogConfig.PrivKey, - PublicKey: ctlogConfig.PubKey, - Password: ctlogConfig.PrivKeyPassword, - } - for i, cert := range ctlogConfig.RootCerts { - fulcioKey := fmt.Sprintf("fulcio-%d", i) - data[fulcioKey] = cert + for _, cert := range rootCerts { + if err := config.AddRootCertificate(cert); err != nil { + return nil, fmt.Errorf("failed to add fulcio root: %v", err) + } } - return data, nil + + return config, nil } diff --git a/internal/controller/ctlog/utils/keys.go b/internal/controller/ctlog/utils/keys.go deleted file mode 100644 index 00366615d..000000000 --- a/internal/controller/ctlog/utils/keys.go +++ /dev/null @@ -1,111 +0,0 @@ -package utils - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/rand" - "crypto/x509" - "encoding/pem" - "fmt" -) - -const ( - curveType = "p256" -) - -type PrivateKeyConfig struct { - PrivateKey []byte - PrivateKeyPass []byte - PublicKey []byte -} - -func CreatePrivateKey() (*PrivateKeyConfig, error) { - key, err := ecdsa.GenerateKey(supportedCurves[curveType], rand.Reader) - if err != nil { - return nil, fmt.Errorf("failed to generate private key: %w", err) - } - - mKey, err := x509.MarshalECPrivateKey(key) - if err != nil { - return nil, err - } - - mPubKey, err := x509.MarshalPKIXPublicKey(key.Public()) - if err != nil { - return nil, err - } - - var pemKey bytes.Buffer - err = pem.Encode(&pemKey, &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: mKey, - }) - if err != nil { - return nil, err - } - - var pemPubKey bytes.Buffer - err = pem.Encode(&pemPubKey, &pem.Block{ - Type: "PUBLIC KEY", - Bytes: mPubKey, - }) - if err != nil { - return nil, err - } - - return &PrivateKeyConfig{ - PrivateKey: pemKey.Bytes(), - PublicKey: pemPubKey.Bytes(), - }, nil -} - -func GeneratePublicKey(certConfig *PrivateKeyConfig) (*PrivateKeyConfig, error) { - var signer crypto.Signer - var priv crypto.PrivateKey - var err error - var ok bool - - privatePEMBlock, _ := pem.Decode(certConfig.PrivateKey) - if privatePEMBlock == nil { - return nil, fmt.Errorf("failed to decode private key") - } - - if x509.IsEncryptedPEMBlock(privatePEMBlock) { //nolint:staticcheck - if certConfig.PrivateKeyPass == nil { - return nil, fmt.Errorf("can't find private key password") - } - privatePEMBlock.Bytes, err = x509.DecryptPEMBlock(privatePEMBlock, certConfig.PrivateKeyPass) //nolint:staticcheck - if err != nil { - return nil, fmt.Errorf("failed to decrypt private key: %w", err) - } - } - - if priv, err = x509.ParsePKCS8PrivateKey(privatePEMBlock.Bytes); err != nil { - // Try it as RSA - if priv, err = x509.ParsePKCS1PrivateKey(privatePEMBlock.Bytes); err != nil { - if priv, err = x509.ParseECPrivateKey(privatePEMBlock.Bytes); err != nil { - return nil, fmt.Errorf("failed to parse private key PEM: %w", err) - } - } - } - - if signer, ok = priv.(crypto.Signer); !ok { - return nil, fmt.Errorf("failed to convert to crypto.Signer") - } - - mPubKey, err := x509.MarshalPKIXPublicKey(signer.Public()) - if err != nil { - return nil, err - } - var pemPubKey bytes.Buffer - err = pem.Encode(&pemPubKey, &pem.Block{ - Type: "PUBLIC KEY", - Bytes: mPubKey, - }) - if err != nil { - return nil, err - } - certConfig.PublicKey = pemPubKey.Bytes() - return certConfig, nil -} diff --git a/internal/controller/ctlog/utils/signer_key.go b/internal/controller/ctlog/utils/signer_key.go new file mode 100644 index 000000000..5df8004b2 --- /dev/null +++ b/internal/controller/ctlog/utils/signer_key.go @@ -0,0 +1,172 @@ +package utils + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + + "github.com/securesign/operator/api/v1alpha1" + k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + curveType = "p256" +) + +var ( + ErrPrivateKeyPassword = errors.New("failed to find private key password") + ErrDecodePrivateKey = errors.New("failed to decode private key") + ErrResolvePrivateKey = errors.New("failed to resolve private key") + ErrResolvePrivateKeyPassword = errors.New("failed to resolve private key password") +) + +type SignerKey struct { + privateKey *ecdsa.PrivateKey + password []byte +} + +func (s SignerKey) PublicKey() (PKIX, error) { + return x509.MarshalPKIXPublicKey(s.privateKey.Public()) +} + +func (s SignerKey) PublicKeyPEM() (PEM, error) { + mPubKey, err := s.PublicKey() + if err != nil { + return nil, err + } + var pemPubKey bytes.Buffer + err = pem.Encode(&pemPubKey, &pem.Block{ + Type: "PUBLIC KEY", + Bytes: mPubKey, + }) + + if err != nil { + return nil, err + } + return pemPubKey.Bytes(), nil +} + +func (s SignerKey) PrivateKey() (PKIX, error) { + return x509.MarshalECPrivateKey(s.privateKey) +} + +func (s SignerKey) PrivateKeyPEM() (PEM, error) { + mKey, err := s.PrivateKey() + if err != nil { + return nil, err + } + + var block *pem.Block + if s.PrivateKeyPassword() != nil { + block, err = x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", mKey, s.PrivateKeyPassword(), x509.PEMCipherAES256) //nolint:staticcheck + if err != nil { + return nil, err + } + } else { + block = &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: mKey, + } + } + + var pemKey bytes.Buffer + err = pem.Encode(&pemKey, block) + if err != nil { + return nil, err + } + + return pemKey.Bytes(), nil +} + +func (s SignerKey) PrivateKeyPassword() []byte { + return s.password +} + +func NewSignerConfig(options ...func(*SignerKey) error) (*SignerKey, error) { + if len(options) == 0 { + options = []func(*SignerKey) error{ + WithGeneratedKey(), + } + } + + config := &SignerKey{} + for _, option := range options { + err := option(config) + if err != nil { + return config, err + } + } + + return config, nil +} + +func WithGeneratedKey() func(*SignerKey) error { + return func(s *SignerKey) error { + key, err := ecdsa.GenerateKey(supportedCurves[curveType], rand.Reader) + if err != nil { + return err + } + s.privateKey = key + + return nil + } +} + +func WithPrivateKeyFromPEM(key PEM, password []byte) func(*SignerKey) error { + return func(s *SignerKey) error { + s.password = password + + var err error + + block, _ := pem.Decode(key) + if block == nil { + return ErrDecodePrivateKey + } + + if x509.IsEncryptedPEMBlock(block) { //nolint:staticcheck + if len(password) == 0 { + return ErrPrivateKeyPassword + } + + block.Bytes, err = x509.DecryptPEMBlock(block, password) //nolint:staticcheck + if err != nil { + return err + } + } + + if s.privateKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { + return err + } + + return nil + } +} + +func ResolveSignerConfig(client client.Client, instance *v1alpha1.CTlog) (*SignerKey, error) { + var ( + private, password []byte + err error + config *SignerKey + ) + + private, err = k8sutils.GetSecretData(client, instance.Namespace, instance.Status.PrivateKeyRef) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrResolvePrivateKey, err) + } + if instance.Status.PrivateKeyPasswordRef != nil { + password, err = k8sutils.GetSecretData(client, instance.Namespace, instance.Status.PrivateKeyPasswordRef) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrResolvePrivateKeyPassword, err) + } + } + config, err = NewSignerConfig(WithPrivateKeyFromPEM(private, password)) + if err != nil || config == nil { + return nil, err + } + return config, nil +} diff --git a/internal/controller/ctlog/utils/signer_key_test.go b/internal/controller/ctlog/utils/signer_key_test.go new file mode 100644 index 000000000..397f71341 --- /dev/null +++ b/internal/controller/ctlog/utils/signer_key_test.go @@ -0,0 +1,141 @@ +package utils + +import ( + "crypto/x509" + _ "embed" + "testing" + + "github.com/onsi/gomega" +) + +var ( + //go:embed testdata/private_key.pem + privateKey PEM + + //go:embed testdata/public_key.pem + publicKey PEM + + //go:embed testdata/private_key_pass.pem + privatePassKey PEM +) + +func TestSignerConfig(t *testing.T) { + type args struct { + options []func(*SignerKey) error + } + tests := []struct { + name string + args args + verify func(gomega.Gomega, *SignerKey, error) + }{ + { + name: "empty", + args: args{ + options: []func(*SignerKey) error{}, + }, + verify: func(g gomega.Gomega, config *SignerKey, err error) { + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config.privateKey).NotTo(gomega.BeNil()) + g.Expect(config.password).To(gomega.BeNil()) + + pr, err := config.PrivateKeyPEM() + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(pr).ToNot(gomega.BeNil()) + + pub, err := config.PublicKeyPEM() + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(pub).ToNot(gomega.BeNil()) + }, + }, + { + name: "generated signer key", + args: args{ + options: []func(*SignerKey) error{ + WithGeneratedKey(), + }, + }, + verify: func(g gomega.Gomega, config *SignerKey, err error) { + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config.privateKey).NotTo(gomega.BeNil()) + g.Expect(config.password).To(gomega.BeNil()) + + pr, err := config.PrivateKeyPEM() + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(pr).ToNot(gomega.BeNil()) + + pub, err := config.PublicKeyPEM() + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(pub).ToNot(gomega.BeNil()) + }, + }, + { + name: "from EC PRIVATE KEY PEM", + args: args{ + options: []func(*SignerKey) error{ + WithPrivateKeyFromPEM(privateKey, nil), + }, + }, + verify: func(g gomega.Gomega, config *SignerKey, err error) { + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config.privateKey).NotTo(gomega.BeNil()) + g.Expect(config.password).To(gomega.BeNil()) + + g.Expect(config.PrivateKeyPEM()).To(gomega.Equal(privateKey)) + g.Expect(config.PublicKeyPEM()).To(gomega.Equal(publicKey)) + }, + }, + { + name: "from encrypted EC PRIVATE KEY PEM", + args: args{ + options: []func(*SignerKey) error{ + WithPrivateKeyFromPEM(privatePassKey, []byte("changeit")), + }, + }, + verify: func(g gomega.Gomega, config *SignerKey, err error) { + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config.privateKey).NotTo(gomega.BeNil()) + g.Expect(config.password).NotTo(gomega.BeNil()) + + pr, err := config.PrivateKeyPEM() + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(pr).ToNot(gomega.BeNil()) + g.Expect(string(pr)).To(gomega.ContainSubstring("DEK-Info: AES-256-CBC")) + + pub, err := config.PublicKeyPEM() + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(pub).To(gomega.Equal(publicKey)) + }, + }, + { + name: "incorrect password", + args: args{ + options: []func(*SignerKey) error{ + WithPrivateKeyFromPEM(privatePassKey, []byte("incorrect")), + }, + }, + verify: func(g gomega.Gomega, config *SignerKey, err error) { + g.Expect(err).To(gomega.HaveOccurred()) + g.Expect(err).To(gomega.MatchError(x509.IncorrectPasswordError)) + }, + }, + { + name: "incorrect key", + args: args{ + options: []func(*SignerKey) error{ + WithPrivateKeyFromPEM([]byte("invalid data"), nil), + }, + }, + verify: func(g gomega.Gomega, config *SignerKey, err error) { + g.Expect(err).To(gomega.HaveOccurred()) + g.Expect(err).To(gomega.MatchError(ErrDecodePrivateKey)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := gomega.NewWithT(t) + got, err := NewSignerConfig(tt.args.options...) + tt.verify(g, got, err) + }) + } +} diff --git a/internal/controller/ctlog/utils/testdata/private_key.pem b/internal/controller/ctlog/utils/testdata/private_key.pem new file mode 100644 index 000000000..1c8efc93a --- /dev/null +++ b/internal/controller/ctlog/utils/testdata/private_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJnHDmCJcNZkiFc1dfPV641tHF//+VmmaB7lbEwyjQRtoAoGCCqGSM49 +AwEHoUQDQgAEGl5VsWbX5hQP9/qW9ODF8PQoR42EFuac8AzUiaGekShMR8zQLW8z +8BBo0iUxPeJEIOYzqC/3qTyiKe0nHxff6g== +-----END EC PRIVATE KEY----- diff --git a/internal/controller/ctlog/utils/testdata/private_key_pass.pem b/internal/controller/ctlog/utils/testdata/private_key_pass.pem new file mode 100644 index 000000000..84d1ea22e --- /dev/null +++ b/internal/controller/ctlog/utils/testdata/private_key_pass.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,CD46D8406445C428F69BB8BBE50480E2 + +FTPDStHxGzG2XIoPVo0WIbDzy/3vJW0Va7hodkU9V3DHgo0cqRPXPW4qlkQjjRbJ +p9hn+pceMHbNuWOK/iCGKTW9OMWeJqLs0N8WE/xp9GpH9AnrtLD2Jfa9XowHIHU3 +beliLg+MxjnqgbMPD0o5jqvlr8j33zRqjlqaiJcH8w8= +-----END EC PRIVATE KEY----- diff --git a/internal/controller/ctlog/utils/testdata/public_key.pem b/internal/controller/ctlog/utils/testdata/public_key.pem new file mode 100644 index 000000000..64fc59f21 --- /dev/null +++ b/internal/controller/ctlog/utils/testdata/public_key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGl5VsWbX5hQP9/qW9ODF8PQoR42E +Fuac8AzUiaGekShMR8zQLW8z8BBo0iUxPeJEIOYzqC/3qTyiKe0nHxff6g== +-----END PUBLIC KEY----- diff --git a/internal/controller/ctlog/utils/types.go b/internal/controller/ctlog/utils/types.go new file mode 100644 index 000000000..7ce7f83bb --- /dev/null +++ b/internal/controller/ctlog/utils/types.go @@ -0,0 +1,7 @@ +package utils + +type RootCertificate []byte + +type PEM []byte + +type PKIX []byte diff --git a/test/e2e/support/common.go b/test/e2e/support/common.go index 25ae57e32..a0b5688c7 100644 --- a/test/e2e/support/common.go +++ b/test/e2e/support/common.go @@ -123,19 +123,20 @@ func DumpNamespace(ctx context.Context, cli client.Client, ns string) { k8s := map[string]logTarget{} toDump := map[string]client.ObjectList{ - "securesign.yaml": &v1alpha1.SecuresignList{}, - "fulcio.yaml": &v1alpha1.FulcioList{}, - "rekor.yaml": &v1alpha1.RekorList{}, - "tuf.yaml": &v1alpha1.TufList{}, - "ctlog.yaml": &v1alpha1.CTlogList{}, - "trillian.yaml": &v1alpha1.TrillianList{}, - "tsa.yaml": &v1alpha1.TimestampAuthorityList{}, - "pod.yaml": &v1.PodList{}, - "configmap.yaml": &v1.ConfigMapList{}, - "deployment.yaml": &v12.DeploymentList{}, - "job.yaml": &v13.JobList{}, - "cronjob.yaml": &v13.CronJobList{}, - "event.yaml": &v1.EventList{}, + "securesign.yaml": &v1alpha1.SecuresignList{}, + "fulcio.yaml": &v1alpha1.FulcioList{}, + "rekor.yaml": &v1alpha1.RekorList{}, + "tuf.yaml": &v1alpha1.TufList{}, + "ctlog.yaml": &v1alpha1.CTlogList{}, + "trillian.yaml": &v1alpha1.TrillianList{}, + "tsa.yaml": &v1alpha1.TimestampAuthorityList{}, + "pod.yaml": &v1.PodList{}, + "configmap.yaml": &v1.ConfigMapList{}, + "deployment.yaml": &v12.DeploymentList{}, + "replica_set.yaml": &v12.ReplicaSetList{}, + "job.yaml": &v13.JobList{}, + "cronjob.yaml": &v13.CronJobList{}, + "event.yaml": &v1.EventList{}, } core.GinkgoWriter.Println("----------------------- Dumping namespace " + ns + " -----------------------") diff --git a/test/e2e/update/ctlog_test.go b/test/e2e/update/ctlog_test.go index ea8da81d1..cf0607260 100644 --- a/test/e2e/update/ctlog_test.go +++ b/test/e2e/update/ctlog_test.go @@ -98,8 +98,8 @@ var _ = Describe("CTlog update", Ordered, func() { Eventually(func(g Gomega) string { ctl := ctlog.Get(ctx, cli, namespace.Name, s.Name)() g.Expect(ctl).NotTo(BeNil()) - return meta.FindStatusCondition(ctl.Status.Conditions, constants.Ready).Reason - }).Should(Equal(constants.Creating)) + return meta.FindStatusCondition(ctl.Status.Conditions, ctlogAction.ServerConfigCondition).Reason + }).Should(Equal(constants.Pending)) }) It("created my-ctlog-secret", func() {