diff --git a/.github/workflows/sign-agent-card.yml b/.github/workflows/sign-agent-card.yml new file mode 100644 index 00000000..5fce586d --- /dev/null +++ b/.github/workflows/sign-agent-card.yml @@ -0,0 +1,97 @@ +name: Sign AgentCard with Sigstore + +on: + workflow_dispatch: # Manual trigger for demo + push: + branches: ['**'] # Any branch (for demo purposes) + paths: + - 'demo/sigstore/unsigned-agent-card.json' + +permissions: + id-token: write # Required for OIDC token (keyless signing) + contents: read + actions: read + +jobs: + sign: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Create canonical AgentCard JSON + run: | + # Remove signatures field and sort keys for canonical form + jq -S 'del(.signatures)' demo/sigstore/unsigned-agent-card.json > canonical-card.json + echo "=== Canonical AgentCard JSON ===" + cat canonical-card.json + + - name: Sign with Sigstore (keyless) + run: | + echo "=== Signing with Sigstore (keyless via GitHub OIDC) ===" + cosign sign-blob \ + --yes \ + --output-signature signature.sig \ + --output-certificate certificate.pem \ + canonical-card.json + + echo "" + echo "=== Fulcio Certificate ===" + openssl x509 -in certificate.pem -text -noout | head -30 + + - name: Assemble signed AgentCard + run: | + echo "=== Assembling Signed AgentCard ===" + + # Create base64url-encoded protected header + PROTECTED=$(echo -n '{"alg":"ES256","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=') + + # Read signature and convert to base64url + SIGNATURE=$(cat signature.sig | tr '+/' '-_' | tr -d '=' | tr -d '\n') + + # Convert certificate to base64 DER for x5c + X5C=$(openssl x509 -in certificate.pem -outform DER | base64 -w0) + + # Get current timestamp + TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # For now use a placeholder index (cosign doesn't easily expose this) + # The actual index can be found in Rekor after the fact + INDEX=0 + + # Assemble the signed card + jq --arg protected "$PROTECTED" \ + --arg signature "$SIGNATURE" \ + --arg x5c "$X5C" \ + --argjson rekor_index "$INDEX" \ + --arg timestamp "$TIMESTAMP" \ + '. + {signatures: [{protected: $protected, signature: $signature, header: {x5c: [$x5c], rekor_log_index: $rekor_index, timestamp: $timestamp}}]}' \ + demo/sigstore/unsigned-agent-card.json > signed-agent-card.json + + echo "" + echo "=== Signed AgentCard ===" + cat signed-agent-card.json | jq . + + - name: Upload signed card as artifact + uses: actions/upload-artifact@v4 + with: + name: signed-weather-agent-card + path: | + signed-agent-card.json + certificate.pem + signature.sig + retention-days: 30 + + - name: Summary + run: | + echo "## Sigstore Signing Complete! :white_check_mark:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**AgentCard:** WeatherAgent v1.0.0" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "1. Download the \`signed-weather-agent-card\` artifact" >> $GITHUB_STEP_SUMMARY + echo "2. Deploy to your Kubernetes cluster with kagenti-operator" >> $GITHUB_STEP_SUMMARY + echo "3. The operator will verify the Sigstore signature" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index eecd4e0e..3fea0508 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ secrets.* # IDE and editor files .idea/ .vscode/ +.cursor/ *.swp *.swo *~ @@ -47,6 +48,7 @@ Thumbs.db # Build artifacts **/dist/ **/bin/ +kagenti-operator/main # Spurious controller-gen CRD artifact kagenti-operator/config/crd/bases/_.yaml diff --git a/charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml b/charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml index 4efde13f..593610f5 100644 --- a/charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml +++ b/charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml @@ -38,6 +38,10 @@ spec: jsonPath: .status.conditions[?(@.type=='Verified')].status name: Verified type: string + - description: Sigstore Bundle Verified + jsonPath: .status.sigstoreBundleVerified + name: Sigstore + type: boolean - description: Identity Bound jsonPath: .status.bindingStatus.bound name: Bound @@ -105,6 +109,21 @@ spec: pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$ type: string type: object + sigstoreVerification: + description: |- + SigstoreVerification optionally overrides operator-level Sigstore identity + constraints for supply-chain bundle verification on SignedAgentCard documents. + properties: + certificateIdentity: + description: CertificateIdentity is the expected OIDC subject + in the Fulcio certificate SAN (exact match unless paired with + regex elsewhere). + type: string + certificateOIDCIssuer: + description: CertificateOIDCIssuer is the expected OIDC issuer + URL (e.g. https://token.actions.githubusercontent.com). + type: string + type: object syncPeriod: default: 30s description: SyncPeriod is how often to re-fetch the agent card (e.g., @@ -424,6 +443,10 @@ spec: protocol: description: Protocol is the detected agent protocol (e.g., "a2a") type: string + rekorLogIndex: + description: RekorLogIndex is the transparency log index when available + from the bundle. + type: string signatureIdentityMatch: description: SignatureIdentityMatch is true when both signature and identity binding pass. @@ -440,6 +463,22 @@ spec: description: SignatureVerificationDetails contains details about the last signature verification type: string + sigstoreBundleVerified: + description: SigstoreBundleVerified is true when SignedAgentCard attestations.signatureBundle + verifies successfully. + type: boolean + sigstoreIdentity: + description: SigstoreIdentity is the verified Fulcio signing identity + when Sigstore verification succeeds. + type: string + slsaCommitSHA: + description: SLSACommitSHA from provenanceBundle when present and + parsed. + type: string + slsaRepository: + description: SLSARepository from provenanceBundle when present and + parsed. + type: string targetRef: description: |- TargetRef contains the resolved reference to the backing workload. diff --git a/charts/kagenti-operator/templates/manager/manager.yaml b/charts/kagenti-operator/templates/manager/manager.yaml index 03d4ec68..21cbaea6 100644 --- a/charts/kagenti-operator/templates/manager/manager.yaml +++ b/charts/kagenti-operator/templates/manager/manager.yaml @@ -89,6 +89,30 @@ spec: - "--credential-wait-timeout={{ .Values.authbridgeConfig.credentialWaitTimeout }}" {{- end }} {{- end }} + {{- if .Values.sigstore.cardVerification.enabled }} + - "--enable-sigstore-verification=true" + {{- if .Values.sigstore.cardVerification.auditMode }} + - "--sigstore-audit-mode=true" + {{- end }} + {{- if .Values.sigstore.cardVerification.certificateIdentity }} + - "--sigstore-certificate-identity={{ .Values.sigstore.cardVerification.certificateIdentity }}" + {{- end }} + {{- if .Values.sigstore.cardVerification.certificateOIDCIssuer }} + - "--sigstore-certificate-oidc-issuer={{ .Values.sigstore.cardVerification.certificateOIDCIssuer }}" + {{- end }} + {{- if .Values.sigstore.cardVerification.staging }} + - "--sigstore-staging=true" + {{- end }} + {{- if .Values.sigstore.cardVerification.trustedRoot.configMapName }} + - "--sigstore-trusted-root-configmap={{ .Values.sigstore.cardVerification.trustedRoot.configMapName }}" + {{- end }} + {{- if .Values.sigstore.cardVerification.trustedRoot.configMapNamespace }} + - "--sigstore-trusted-root-configmap-namespace={{ .Values.sigstore.cardVerification.trustedRoot.configMapNamespace }}" + {{- end }} + {{- if .Values.sigstore.cardVerification.trustedRoot.configMapKey }} + - "--sigstore-trusted-root-configmap-key={{ .Values.sigstore.cardVerification.trustedRoot.configMapKey }}" + {{- end }} + {{- end }} command: - {{ .Values.controllerManager.container.cmd }} image: {{ .Values.controllerManager.container.image.repository }}:{{ .Values.controllerManager.container.image.tag }} diff --git a/charts/kagenti-operator/values.yaml b/charts/kagenti-operator/values.yaml index 352d1368..46777afa 100644 --- a/charts/kagenti-operator/values.yaml +++ b/charts/kagenti-operator/values.yaml @@ -158,6 +158,19 @@ signatureVerification: # How far before SVID expiry to trigger proactive workload restart svidExpiryGracePeriod: "30m" +# Supply-chain: sigstore-a2a SignedAgentCard bundle verification (Rekor / Fulcio) +sigstore: + cardVerification: + enabled: false + auditMode: false + certificateIdentity: "" + certificateOIDCIssuer: "" + staging: false + trustedRoot: + configMapName: "" + configMapNamespace: "" + configMapKey: "trusted-root.json" + # Feature gates — highest-priority layer in the injection precedence chain. # Set globalEnabled to false to disable ALL sidecar injection (kill switch). # Set individual gates to false to disable specific sidecars cluster-wide. diff --git a/kagenti-operator/api/v1alpha1/agentcard_types.go b/kagenti-operator/api/v1alpha1/agentcard_types.go index 85eab811..7324932c 100644 --- a/kagenti-operator/api/v1alpha1/agentcard_types.go +++ b/kagenti-operator/api/v1alpha1/agentcard_types.go @@ -36,6 +36,22 @@ type AgentCardSpec struct { // IdentityBinding specifies SPIFFE identity binding configuration // +optional IdentityBinding *IdentityBinding `json:"identityBinding,omitempty"` + + // SigstoreVerification optionally overrides operator-level Sigstore identity + // constraints for supply-chain bundle verification on SignedAgentCard documents. + // +optional + SigstoreVerification *SigstoreVerification `json:"sigstoreVerification,omitempty"` +} + +// SigstoreVerification configures expected Fulcio certificate identity for Sigstore bundle verification. +type SigstoreVerification struct { + // CertificateIdentity is the expected OIDC subject in the Fulcio certificate SAN (exact match unless paired with regex elsewhere). + // +optional + CertificateIdentity string `json:"certificateIdentity,omitempty"` + + // CertificateOIDCIssuer is the expected OIDC issuer URL (e.g. https://token.actions.githubusercontent.com). + // +optional + CertificateOIDCIssuer string `json:"certificateOIDCIssuer,omitempty"` } // IdentityBinding configures workload identity binding for an AgentCard. @@ -131,6 +147,26 @@ type AgentCardStatus struct { // BindingStatus contains the result of identity binding evaluation // +optional BindingStatus *BindingStatus `json:"bindingStatus,omitempty"` + + // SigstoreBundleVerified is true when SignedAgentCard attestations.signatureBundle verifies successfully. + // +optional + SigstoreBundleVerified *bool `json:"sigstoreBundleVerified,omitempty"` + + // SigstoreIdentity is the verified Fulcio signing identity when Sigstore verification succeeds. + // +optional + SigstoreIdentity string `json:"sigstoreIdentity,omitempty"` + + // RekorLogIndex is the transparency log index when available from the bundle. + // +optional + RekorLogIndex string `json:"rekorLogIndex,omitempty"` + + // SLSARepository from provenanceBundle when present and parsed. + // +optional + SLSARepository string `json:"slsaRepository,omitempty"` + + // SLSACommitSHA from provenanceBundle when present and parsed. + // +optional + SLSACommitSHA string `json:"slsaCommitSHA,omitempty"` } // BindingStatus represents the result of identity binding evaluation @@ -348,6 +384,7 @@ type SkillParameter struct { // +kubebuilder:printcolumn:name="Target",type="string",JSONPath=".status.targetRef.name",description="Target Workload" // +kubebuilder:printcolumn:name="Agent",type="string",JSONPath=".status.card.name",description="Agent Name" // +kubebuilder:printcolumn:name="Verified",type="string",JSONPath=".status.conditions[?(@.type=='Verified')].status",description="Identity Verified" +// +kubebuilder:printcolumn:name="Sigstore",type="boolean",JSONPath=".status.sigstoreBundleVerified",description="Sigstore Bundle Verified" // +kubebuilder:printcolumn:name="Bound",type="boolean",JSONPath=".status.bindingStatus.bound",description="Identity Bound" // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status",description="Sync Status" // +kubebuilder:printcolumn:name="LastSync",type="date",JSONPath=".status.lastSyncTime",description="Last Sync Time" diff --git a/kagenti-operator/api/v1alpha1/zz_generated.deepcopy.go b/kagenti-operator/api/v1alpha1/zz_generated.deepcopy.go index 5a87d25b..3ee43652 100644 --- a/kagenti-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/kagenti-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -204,6 +204,11 @@ func (in *AgentCardSpec) DeepCopyInto(out *AgentCardSpec) { *out = new(IdentityBinding) **out = **in } + if in.SigstoreVerification != nil { + in, out := &in.SigstoreVerification, &out.SigstoreVerification + *out = new(SigstoreVerification) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCardSpec. @@ -255,6 +260,11 @@ func (in *AgentCardStatus) DeepCopyInto(out *AgentCardStatus) { *out = new(BindingStatus) (*in).DeepCopyInto(*out) } + if in.SigstoreBundleVerified != nil { + in, out := &in.SigstoreBundleVerified, &out.SigstoreBundleVerified + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCardStatus. @@ -577,6 +587,21 @@ func (in *SignatureHeader) DeepCopy() *SignatureHeader { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SigstoreVerification) DeepCopyInto(out *SigstoreVerification) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SigstoreVerification. +func (in *SigstoreVerification) DeepCopy() *SigstoreVerification { + if in == nil { + return nil + } + out := new(SigstoreVerification) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SkillParameter) DeepCopyInto(out *SkillParameter) { *out = *in diff --git a/kagenti-operator/cmd/configmap_cache_test.go b/kagenti-operator/cmd/configmap_cache_test.go index 65eddb72..c470f126 100644 --- a/kagenti-operator/cmd/configmap_cache_test.go +++ b/kagenti-operator/cmd/configmap_cache_test.go @@ -79,7 +79,7 @@ func TestConfigMapCacheVisibility(t *testing.T) { // 4. Build the scoped cache config (function under test) and start a // manager whose ConfigMap informers are restricted to those selectors. - cmCacheNamespaces := buildConfigMapCacheNamespaces(true, spireBundleName, spireNS) + cmCacheNamespaces := buildConfigMapCacheNamespaces(true, spireBundleName, spireNS, false, "", "") mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: clientgoscheme.Scheme, diff --git a/kagenti-operator/cmd/main.go b/kagenti-operator/cmd/main.go index e6ddde42..cef42202 100644 --- a/kagenti-operator/cmd/main.go +++ b/kagenti-operator/cmd/main.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -202,6 +203,32 @@ func main() { flag.BoolVar(&enableAuthbridgeConfig, "enable-authbridge-config", true, "Reconcile authbridge-config ConfigMap in namespaces labeled kagenti-enabled=true") + var enableSigstoreVerification bool + var sigstoreAuditMode bool + var sigstoreCertificateIdentity string + var sigstoreCertificateOIDCIssuer string + var sigstoreTrustedRootConfigMap string + var sigstoreTrustedRootConfigMapNamespace string + var sigstoreTrustedRootConfigMapKey string + var sigstoreStaging bool + + flag.BoolVar(&enableSigstoreVerification, "enable-sigstore-verification", false, + "Enable SignedAgentCard (sigstore-a2a) bundle verification") + flag.BoolVar(&sigstoreAuditMode, "sigstore-audit-mode", false, + "When true, log Sigstore bundle verification failures but do not block reconciliation") + flag.StringVar(&sigstoreCertificateIdentity, "sigstore-certificate-identity", "", + "Expected Fulcio certificate identity (SAN), e.g. GitHub workflow identity") + flag.StringVar(&sigstoreCertificateOIDCIssuer, "sigstore-certificate-oidc-issuer", "", + "Expected OIDC issuer for Fulcio (e.g. https://token.actions.githubusercontent.com)") + flag.StringVar(&sigstoreTrustedRootConfigMap, "sigstore-trusted-root-configmap", "", + "Optional ConfigMap name containing Sigstore trusted_root.json for private deployments") + flag.StringVar(&sigstoreTrustedRootConfigMapNamespace, "sigstore-trusted-root-configmap-namespace", "", + "Namespace of the Sigstore trusted root ConfigMap") + flag.StringVar(&sigstoreTrustedRootConfigMapKey, "sigstore-trusted-root-configmap-key", "trusted-root.json", + "Key within the trusted root ConfigMap") + flag.BoolVar(&sigstoreStaging, "sigstore-staging", false, + "Use Sigstore staging TUF mirror (for cards signed against staging infrastructure)") + opts := zap.Options{ Development: false, } @@ -320,6 +347,7 @@ func main() { cmCacheNamespaces := buildConfigMapCacheNamespaces( requireA2ASignature, spireTrustBundleConfigMapName, spireTrustBundleConfigMapNS, + enableSigstoreVerification, sigstoreTrustedRootConfigMap, sigstoreTrustedRootConfigMapNamespace, ) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ @@ -416,6 +444,7 @@ func main() { "auditMode", signatureAuditMode) } + // Feature 1: Setup authenticated fetcher for verified fetch (mTLS via SPIFFE) agentFetcher := agentcard.NewConfigMapFetcher(mgr.GetAPIReader()) var authenticatedFetcher agentcard.AuthenticatedFetcher @@ -453,21 +482,80 @@ func main() { } } - agentCardReconciler := &controller.AgentCardReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("agentcard-controller"), - AgentFetcher: agentFetcher, - AuthenticatedFetcher: authenticatedFetcher, - EnableVerifiedFetch: enableVerifiedFetch, - SignatureProvider: sigProvider, - RequireSignature: requireA2ASignature, - SignatureAuditMode: signatureAuditMode, - SpireTrustDomain: spireTrustDomain, - SVIDExpiryGracePeriod: svidExpiryGracePeriod, - } - - if err = agentCardReconciler.SetupWithManager(mgr); err != nil { + // Feature 2: Setup Sigstore bundle verifier for SignedAgentCard verification + var bundleVerifier signature.BundleVerifier + if enableSigstoreVerification { + if sigstoreCertificateIdentity == "" || sigstoreCertificateOIDCIssuer == "" { + setupLog.Error(errors.New("missing required flags"), + "--sigstore-certificate-identity and --sigstore-certificate-oidc-issuer "+ + "are required when --enable-sigstore-verification=true") + os.Exit(1) + } + var trustedRootJSON []byte + if sigstoreTrustedRootConfigMap != "" { + if sigstoreTrustedRootConfigMapNamespace == "" { + setupLog.Error(errors.New("missing namespace"), + "--sigstore-trusted-root-configmap-namespace is required "+ + "when using --sigstore-trusted-root-configmap") + os.Exit(1) + } + bootstrapClient, cliErr := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme}) + if cliErr != nil { + setupLog.Error(cliErr, "unable to create client for Sigstore trusted root ConfigMap") + os.Exit(1) + } + readCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + var cm corev1.ConfigMap + if err := bootstrapClient.Get(readCtx, types.NamespacedName{ + Namespace: sigstoreTrustedRootConfigMapNamespace, + Name: sigstoreTrustedRootConfigMap, + }, &cm); err != nil { + setupLog.Error(err, "failed to read Sigstore trusted root ConfigMap") + os.Exit(1) + } + raw := cm.Data[sigstoreTrustedRootConfigMapKey] + if raw == "" { + setupLog.Error(errors.New("empty ConfigMap data"), + "Sigstore trusted root ConfigMap missing key", "key", sigstoreTrustedRootConfigMapKey) + os.Exit(1) + } + trustedRootJSON = []byte(raw) + } + sigCfg := &signature.SigstoreConfig{ + TrustedRootJSON: trustedRootJSON, + UseStagingTUF: sigstoreStaging, + OIDCIssuer: sigstoreCertificateOIDCIssuer, + CertificateIdentity: sigstoreCertificateIdentity, + } + var bvErr error + bundleVerifier, bvErr = signature.NewSigstoreProvider(sigCfg) + if bvErr != nil { + setupLog.Error(bvErr, "unable to create Sigstore bundle verifier") + os.Exit(1) + } + setupLog.Info("Sigstore SignedAgentCard verification enabled", + "auditMode", sigstoreAuditMode, + "stagingTUF", sigstoreStaging, + "customTrustedRoot", len(trustedRootJSON) > 0) + } + + if err = (&controller.AgentCardReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("agentcard-controller"), + AgentFetcher: agentFetcher, + AuthenticatedFetcher: authenticatedFetcher, + EnableVerifiedFetch: enableVerifiedFetch, + SignatureProvider: sigProvider, + RequireSignature: requireA2ASignature, + SignatureAuditMode: signatureAuditMode, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: enableSigstoreVerification, + SigstoreAuditMode: sigstoreAuditMode, + SpireTrustDomain: spireTrustDomain, + SVIDExpiryGracePeriod: svidExpiryGracePeriod, + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AgentCard") os.Exit(1) } @@ -510,7 +598,7 @@ func main() { Client: mgr.GetClient(), APIReader: mgr.GetAPIReader(), Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("agentruntime-controller"), + Recorder: mgr.GetEventRecorder("agentruntime-controller"), EnableCardDiscovery: enableCardDiscovery, SpireTrustDomain: spireTrustDomain, GetFeatureGates: featureGateLoader.Get, @@ -532,7 +620,7 @@ func main() { if err = (&controller.MLflowReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("mlflow-controller"), //nolint:staticcheck + Recorder: mgr.GetEventRecorder("mlflow-controller"), MLflowCAFile: mlflowCAFile, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MLflow") @@ -605,13 +693,18 @@ func main() { } } - if err = webhookv1alpha1.SetupAgentCardWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "AgentCard") - os.Exit(1) - } - if err = webhookv1alpha1.SetupAgentRuntimeWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "AgentRuntime") - os.Exit(1) + // Validation webhooks + // For local testing without webhook certificates, set ENABLE_WEBHOOKS=false: + // ENABLE_WEBHOOKS=false ./bin/manager --leader-elect=false [other flags...] + if authBridgeWebhooksEnabled() { + if err = webhookv1alpha1.SetupAgentCardWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AgentCard") + os.Exit(1) + } + if err = webhookv1alpha1.SetupAgentRuntimeWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AgentRuntime") + os.Exit(1) + } } // AuthBridge sidecar injection webhook @@ -747,6 +840,7 @@ func getNamespacesToWatch() map[string]cache.Config { // signature verification is enabled. func buildConfigMapCacheNamespaces( requireA2ASignature bool, spireTrustBundleConfigMapName, spireTrustBundleConfigMapNS string, + enableSigstoreVerification bool, sigstoreTrustedRootCM, sigstoreTrustedRootCMNS string, ) map[string]cache.Config { namespaces := map[string]cache.Config{ controller.ClusterDefaultsNamespace: { @@ -778,5 +872,20 @@ func buildConfigMapCacheNamespaces( } } } + if enableSigstoreVerification && sigstoreTrustedRootCM != "" && sigstoreTrustedRootCMNS != "" { + if _, collision := namespaces[sigstoreTrustedRootCMNS]; collision { + setupLog.Error( + errors.New("namespace collision: --sigstore-trusted-root-configmap-namespace overlaps an existing cache rule"), + "Sigstore trusted root ConfigMap may not be watched efficiently", + "namespace", sigstoreTrustedRootCMNS, + ) + } else { + namespaces[sigstoreTrustedRootCMNS] = cache.Config{ + FieldSelector: fields.SelectorFromSet(fields.Set{ + "metadata.name": sigstoreTrustedRootCM, + }), + } + } + } return namespaces } diff --git a/kagenti-operator/cmd/main_test.go b/kagenti-operator/cmd/main_test.go index 23ef432d..8ae28a2d 100644 --- a/kagenti-operator/cmd/main_test.go +++ b/kagenti-operator/cmd/main_test.go @@ -49,7 +49,7 @@ func TestAuthBridgeWebhooksEnabled(t *testing.T) { func TestBuildConfigMapCacheNamespaces(t *testing.T) { t.Run("base config always includes cluster defaults and namespace defaults", func(t *testing.T) { - result := buildConfigMapCacheNamespaces(false, "", "") + result := buildConfigMapCacheNamespaces(false, "", "", false, "", "") if _, ok := result[controller.ClusterDefaultsNamespace]; !ok { t.Fatalf("expected entry for %s", controller.ClusterDefaultsNamespace) @@ -64,7 +64,7 @@ func TestBuildConfigMapCacheNamespaces(t *testing.T) { t.Run("adds SPIRE trust bundle namespace when signature verification enabled", func(t *testing.T) { const spireNS = "zero-trust-workload-identity-manager" - result := buildConfigMapCacheNamespaces(true, "spire-bundle", spireNS) + result := buildConfigMapCacheNamespaces(true, "spire-bundle", spireNS, false, "", "") spireCfg, ok := result[spireNS] if !ok { @@ -86,7 +86,7 @@ func TestBuildConfigMapCacheNamespaces(t *testing.T) { t.Run("does not add SPIRE entry when flag is false", func(t *testing.T) { const spireNS = "zero-trust-workload-identity-manager" - result := buildConfigMapCacheNamespaces(false, "spire-bundle", spireNS) + result := buildConfigMapCacheNamespaces(false, "spire-bundle", spireNS, false, "", "") if _, ok := result[spireNS]; ok { t.Fatal("expected no SPIRE entry when requireA2ASignature is false") @@ -97,7 +97,7 @@ func TestBuildConfigMapCacheNamespaces(t *testing.T) { }) t.Run("does not add SPIRE entry when namespace is empty", func(t *testing.T) { - result := buildConfigMapCacheNamespaces(true, "spire-bundle", "") + result := buildConfigMapCacheNamespaces(true, "spire-bundle", "", false, "", "") if len(result) != 2 { t.Fatalf("expected 2 entries, got %d", len(result)) @@ -105,7 +105,7 @@ func TestBuildConfigMapCacheNamespaces(t *testing.T) { }) t.Run("namespace collision preserves existing label selector", func(t *testing.T) { - result := buildConfigMapCacheNamespaces(true, "spire-bundle", controller.ClusterDefaultsNamespace) + result := buildConfigMapCacheNamespaces(true, "spire-bundle", controller.ClusterDefaultsNamespace, false, "", "") cfg := result[controller.ClusterDefaultsNamespace] if cfg.LabelSelector == nil { diff --git a/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentcards.yaml b/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentcards.yaml index 4efde13f..593610f5 100644 --- a/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentcards.yaml +++ b/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentcards.yaml @@ -38,6 +38,10 @@ spec: jsonPath: .status.conditions[?(@.type=='Verified')].status name: Verified type: string + - description: Sigstore Bundle Verified + jsonPath: .status.sigstoreBundleVerified + name: Sigstore + type: boolean - description: Identity Bound jsonPath: .status.bindingStatus.bound name: Bound @@ -105,6 +109,21 @@ spec: pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$ type: string type: object + sigstoreVerification: + description: |- + SigstoreVerification optionally overrides operator-level Sigstore identity + constraints for supply-chain bundle verification on SignedAgentCard documents. + properties: + certificateIdentity: + description: CertificateIdentity is the expected OIDC subject + in the Fulcio certificate SAN (exact match unless paired with + regex elsewhere). + type: string + certificateOIDCIssuer: + description: CertificateOIDCIssuer is the expected OIDC issuer + URL (e.g. https://token.actions.githubusercontent.com). + type: string + type: object syncPeriod: default: 30s description: SyncPeriod is how often to re-fetch the agent card (e.g., @@ -424,6 +443,10 @@ spec: protocol: description: Protocol is the detected agent protocol (e.g., "a2a") type: string + rekorLogIndex: + description: RekorLogIndex is the transparency log index when available + from the bundle. + type: string signatureIdentityMatch: description: SignatureIdentityMatch is true when both signature and identity binding pass. @@ -440,6 +463,22 @@ spec: description: SignatureVerificationDetails contains details about the last signature verification type: string + sigstoreBundleVerified: + description: SigstoreBundleVerified is true when SignedAgentCard attestations.signatureBundle + verifies successfully. + type: boolean + sigstoreIdentity: + description: SigstoreIdentity is the verified Fulcio signing identity + when Sigstore verification succeeds. + type: string + slsaCommitSHA: + description: SLSACommitSHA from provenanceBundle when present and + parsed. + type: string + slsaRepository: + description: SLSARepository from provenanceBundle when present and + parsed. + type: string targetRef: description: |- TargetRef contains the resolved reference to the backing workload. diff --git a/kagenti-operator/config/default/kustomization.yaml b/kagenti-operator/config/default/kustomization.yaml index 98dada3e..c89e1466 100644 --- a/kagenti-operator/config/default/kustomization.yaml +++ b/kagenti-operator/config/default/kustomization.yaml @@ -54,6 +54,7 @@ patches: target: kind: Deployment - path: webhook_namespace_selector_patch.yaml +- path: webhook_selector_patch.yaml target: kind: MutatingWebhookConfiguration diff --git a/kagenti-operator/config/default/webhook_selector_patch.yaml b/kagenti-operator/config/default/webhook_selector_patch.yaml new file mode 100644 index 00000000..0b2ad97e --- /dev/null +++ b/kagenti-operator/config/default/webhook_selector_patch.yaml @@ -0,0 +1,32 @@ +# This patch adds namespace and object selectors to the mutating webhook +# to prevent it from intercepting ALL pod CREATEs cluster-wide. +# Only pods in kagenti-enabled namespaces with kagenti.io/type labels are intercepted. +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- name: inject.kagenti.io + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kagenti-operator-system + matchLabels: + kagenti-enabled: "true" + objectSelector: + matchExpressions: + - key: kagenti.io/type + operator: In + values: + - agent + - tool + - key: kagenti.io/inject + operator: NotIn + values: + - disabled + timeoutSeconds: 10 diff --git a/kagenti-operator/config/rbac/role.yaml b/kagenti-operator/config/rbac/role.yaml index d6b9febc..bc7cf1fd 100644 --- a/kagenti-operator/config/rbac/role.yaml +++ b/kagenti-operator/config/rbac/role.yaml @@ -17,13 +17,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - apiGroups: - "" resources: @@ -59,6 +52,14 @@ rules: - get - list - watch +- apiGroups: + - "" + - events.k8s.io + resources: + - events + verbs: + - create + - patch - apiGroups: - agent.kagenti.dev resources: @@ -140,6 +141,22 @@ rules: - get - list - watch +- apiGroups: + - datasciencecluster.opendatahub.io + resources: + - datascienceclusters + verbs: + - get + - list + - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch - apiGroups: - mlflow.kubeflow.org resources: @@ -154,8 +171,10 @@ rules: resources: - mlflows verbs: + - create - get - list + - update - watch - apiGroups: - networking.k8s.io @@ -180,6 +199,11 @@ rules: - operator.openshift.io resources: - networks + verbs: + - get +- apiGroups: + - operator.openshift.io + resources: - zerotrustworkloadidentitymanagers verbs: - get @@ -193,10 +217,35 @@ rules: - list - patch - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - get +- apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - mlflow-operator-mlflow-edit + - mlflow-operator-mlflow-integration + resources: + - clusterroles + verbs: + - bind - apiGroups: - rbac.authorization.k8s.io resources: - rolebindings + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: - roles verbs: - create diff --git a/kagenti-operator/examples/ci-agent-card.json b/kagenti-operator/examples/ci-agent-card.json new file mode 100644 index 00000000..ac1ce815 --- /dev/null +++ b/kagenti-operator/examples/ci-agent-card.json @@ -0,0 +1,19 @@ +{ + "name": "CI Example Agent", + "version": "0.0.1", + "description": "Example agent card for CI signing and verification testing", + "url": "http://example.local:8000", + "defaultInputModes": ["text"], + "defaultOutputModes": ["text"], + "capabilities": { + "streaming": true + }, + "skills": [ + { + "id": "echo", + "name": "Echo", + "description": "Sample skill for CI signing", + "tags": ["example", "test"] + } + ] +} diff --git a/kagenti-operator/go.mod b/kagenti-operator/go.mod index c1aefef4..69d01c8c 100644 --- a/kagenti-operator/go.mod +++ b/kagenti-operator/go.mod @@ -8,10 +8,12 @@ require ( github.com/cert-manager/cert-manager v1.20.2 github.com/fsnotify/fsnotify v1.10.1 github.com/go-logr/logr v1.4.3 + github.com/gowebpki/jcs v1.0.1 github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.41.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 + github.com/sigstore/sigstore-go v1.1.4 github.com/spiffe/go-spiffe/v2 v2.7.0 k8s.io/api v0.36.1 k8s.io/apiextensions-apiserver v0.36.1 @@ -24,48 +26,107 @@ require ( require ( cel.dev/expr v0.25.1 // indirect + cloud.google.com/go/iam v1.11.0 // indirect + cloud.google.com/go/longrunning v1.0.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-oidc/v3 v3.18.0 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gkampitakis/go-snaps v0.5.22 // indirect + github.com/go-chi/chi/v5 v5.3.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/jsonreference v0.21.4 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/analysis v0.25.0 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime v0.29.4 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/strfmt v0.26.2 // indirect + github.com/go-openapi/swag v0.26.0 // indirect + github.com/go-openapi/swag/cmdutils v0.26.0 // indirect + github.com/go-openapi/swag/conv v0.26.0 // indirect + github.com/go-openapi/swag/fileutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 // indirect + github.com/go-openapi/swag/loading v0.26.0 // indirect + github.com/go-openapi/swag/mangling v0.26.0 // indirect + github.com/go-openapi/swag/netutils v0.26.0 // indirect + github.com/go-openapi/swag/stringutils v0.26.0 // indirect + github.com/go-openapi/swag/typeutils v0.26.0 // indirect + github.com/go-openapi/swag/yamlutils v0.26.0 // indirect + github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 // indirect + github.com/go-openapi/testify/v2 v2.5.1 // indirect + github.com/go-openapi/validate v0.25.2 // indirect + github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/cel-go v0.26.0 // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect + github.com/google/go-containerregistry v0.20.7 // indirect + github.com/google/pprof v0.0.0-20260604005048-7023385849c0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/vault/api v1.23.0 // indirect + github.com/in-toto/attestation v1.1.2 // indirect + github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect + github.com/jedisct1/go-minisign v0.0.0-20260527172527-a09352b57a22 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.1 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/letsencrypt/boulder v0.20260608.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/sigstore/protobuf-specs v0.5.1 // indirect + github.com/sigstore/rekor v1.5.2 // indirect + github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/sigstore v1.10.6 // indirect + github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8 // indirect + github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8 // indirect + github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8 // indirect + github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect @@ -73,28 +134,31 @@ require ( go.opentelemetry.io/otel/sdk v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.step.sm/crypto v0.82.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/api v0.284.0 // indirect + google.golang.org/genproto v0.0.0-20260608224507-4308a22a1bab // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.36.1 // indirect k8s.io/component-base v0.36.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect diff --git a/kagenti-operator/go.sum b/kagenti-operator/go.sum index 4a27f4f3..a5658c5b 100644 --- a/kagenti-operator/go.sum +++ b/kagenti-operator/go.sum @@ -1,30 +1,114 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute v1.64.0 h1:7MmuzeAxlG5MOG5PQD2NLtyYR6bWjkvGljRu7pByoRU= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= +cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= +cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= +cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= +cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 h1:MaKvxE6D0KkjOg6Wd9M00iqP5PR0kUxCfiezes4JweM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0/go.mod h1:i2h9fsTFKZorh8RdV2IcSUf/Qj98GlTkrTvUbX/s8as= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= +github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= +github.com/aws/aws-sdk-go-v2/config v1.32.22 h1:Vfvp7+fYKsVCADcWOEllqEV47aIBXhNchvyDFu1B5fY= +github.com/aws/aws-sdk-go-v2/config v1.32.22/go.mod h1:0+H+0nPKbvWltf5vSIGkApv+hGbaQ4FfwTjGIYQREcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.21 h1:0+HscFXtNa4+3buV4IlG6v5lnOdzi5TrpicFGjKHgh4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.21/go.mod h1:UE8+9t5zudFwu5k5ShC1PKArVEdOkQQdCXIHQAVNUcU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 h1:BEfN1sjtiKEdikRDxYkjZNE4tyvw/YbGWCbl3xDZgRw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27/go.mod h1:ISGSFNbOHRS+JV/17yStzRTPBUHHqF92kCpRLLyH3Nk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 h1:eaS9vwQ5ym4Y9S6+G/K3d3lgZhxs9Sldcn/YS7cmdKY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28/go.mod h1:oTdbDr+BMs7gAYrNpD0LDTyqQfv6yOYgTDv46+xbwFY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 h1:rFSsqDfCMPAmG70JOsYqFZCHXkyatoGa1K4YEt/BggQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11/go.mod h1:XG68qW+YLLFH0vnSDCou43Cgj5TeAG83O5NRSJgt04Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 h1:2/pUo42hhVmQcM21ttZoBOLHQymyUH8qEnZGTIuGBT8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27/go.mod h1:p7hwgbwompjCRNTdB3ytlldddNt1rDBgVVMqWEVG1II= +github.com/aws/aws-sdk-go-v2/service/kms v1.53.2 h1:MWZ4SxCMum1Ri0LU68jnhJGMmjt4Q50RD37BS7XKMY4= +github.com/aws/aws-sdk-go-v2/service/kms v1.53.2/go.mod h1:ZNloshpT3zBMbDUwsbbwYP46ADvoJU9/9WGX1WYegwg= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 h1:t6U7sowMfOjTeZXtDOtgEJXsoJyX4MDag+sfWGwUM9M= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.3/go.mod h1:WhO1EH3phjFWValQDsExaxncgEWJsHeoTvuyQAj3jwU= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 h1:TUV8oytPCX1PfVgZn0N8/sPZx7T0YasaMCBHox1erlw= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.1/go.mod h1:tEL1hqCrkgwrDVL04HuLxz1SLUXdh+4kKhWv1pXKeiY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 h1:p9+Fizo2sUB6wI5Yb3K5lmykQAGs5JrKLBV/me6613Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4/go.mod h1:0x10Wy0dVS4Gn552xhHY5th2QdYpfJf44EsfyYGV194= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 h1:r/vUkpLilfCA3sxbRnkHbJejaoVHEdj4FEhv+Zva4DU= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.1/go.mod h1:t01JURC8Fe5M+7R1K0vzIZ2NT04HqvZR+FjlHrHDT2A= +github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= +github.com/aws/smithy-go v1.27.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cert-manager/cert-manager v1.20.2 h1:CimnY00nLqB2lmxhoSuEC4GDMFDK7JCXqyjwMM9ndIQ= github.com/cert-manager/cert-manager v1.20.2/go.mod h1:1g/+a/WK5zWH/dXPZa3dMD3aJQJNRXQu+PN17C6WrOw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= +github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -33,12 +117,12 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= -github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= -github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= -github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z0KY= +github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-snaps v0.5.22 h1:xg9omphRnbDnimMCl1KqznC4krlxOGpkB0vDSfX2P7M= +github.com/gkampitakis/go-snaps v0.5.22/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM= +github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= +github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -48,59 +132,168 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= -github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= +github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo= +github.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= +github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= +github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= +github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260604005048-7023385849c0 h1:h1QTMDl6q9wDvDCJVpKQSjgleGFYnd2fOxmg2K+6BGE= +github.com/google/pprof v0.0.0-20260604005048-7023385849c0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro= +github.com/google/trillian v1.7.3/go.mod h1:qh8iy4x/GvnVXUBd5pK4oncuT1Y9vVYfibQVsR/WpKg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= +github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= +github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= +github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= +github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jedisct1/go-minisign v0.0.0-20260527172527-a09352b57a22 h1:C68TAi+k12EKJCAmsdaERzQ22ZxVE6n+CuB3kOkhQ7c= +github.com/jedisct1/go-minisign v0.0.0-20260527172527-a09352b57a22/go.mod h1:vYVVh81Lqe/TP0sPLjiNYcX9Hxy/YSfkUx96lYJeyKo= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= -github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/letsencrypt/boulder v0.20260608.0 h1:6IMbSzr4XC321Yqph4zokoGW7tdVDvR1on6lI/wJiMk= +github.com/letsencrypt/boulder v0.20260608.0/go.mod h1:SCtxgc9za2EpV67oillMAaAQKdlZBXTRasJLgi9+GBM= +github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= +github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -109,10 +302,19 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag= github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA= github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -129,6 +331,38 @@ github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05Zp github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= +github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= +github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= +github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U= +github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/rekor v1.5.2 h1:k6pX4o1zFAzAvDbXiVIp5IHj1b0wcDaxsbsbNpuRO8o= +github.com/sigstore/rekor v1.5.2/go.mod h1:WkMnITBccOFauPkT6yte74tF5gC83pefKRGZvNOsbjI= +github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= +github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/sigstore v1.10.6 h1:YWhMQfTrJSK80QB1pbxjYeAwGKx+5UwWPPAY9hrPPZg= +github.com/sigstore/sigstore v1.10.6/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8 h1:tofVQ+UWJgad/69I5zbqxdFCN5gpIn9tRQP7iBzIpBw= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8/go.mod h1:73AfJE8H6w5KGCFPBu4x/OG+i1Yxgmh0L/FtV7prd88= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8 h1:8Mt7J36GcUEmbiJaiFhz2tud5ZIgkfVVCe2H/WJCHmw= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8/go.mod h1:YiTpAsxoWXhF9KlLOVWCh7BckN5cYO8X01WufDq1ido= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8 h1:MxpAIMZVzn0Tpbarc9ax1I498oQBp7oYSMgoMSsOmKI= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8/go.mod h1:bnAUEkFNam6STvkVZhptVwWzWR5pS24CEtQ+lhxu7S0= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8 h1:1DGe4/clcdOnkz5MINEczWlmEvjUtZd+AjPPT/cBhQ8= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8/go.mod h1:6IDFhpgxtzqbnzrFkyegbj7RfWwKeRrb3/+xAD1Wp+Y= +github.com/sigstore/timestamp-authority/v2 v2.0.6 h1:1Vh7/SdmLsVLG6Br6/bisd1SnlicfDm0MJYiA+D7Ppw= +github.com/sigstore/timestamp-authority/v2 v2.0.6/go.mod h1:Nk5ucGBDyH0tXAIMZ0prf6xn8qfTnbJhSq+CDabYcfc= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -144,25 +378,51 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.4.2 h1:w7976/W8uTwlsegP5nRymlpjPgrwSh+AXUf85is6nJk= +github.com/theupdateframework/go-tuf/v2 v2.4.2/go.mod h1:JqBrIUnNLAaNq/8GmBcEMFWfAFBbqp/MkJEJseXKbks= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= +github.com/tink-crypto/tink-go-awskms/v3 v3.0.0 h1:XSohRhCkXAVI0iaCnWB/GS05TEmpnKurQmzaY1jzt3Y= +github.com/tink-crypto/tink-go-awskms/v3 v3.0.0/go.mod h1:+7MXsShLzVbSQ6dI0Pe4JuZM52jD1jQ1itAygd/MDsA= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= @@ -179,16 +439,20 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.step.sm/crypto v0.82.0 h1:JOT8b/7Jh4My3mxE4U7UkuaN2sUGkZ8fnjznXaTGoRE= +go.step.sm/crypto v0.82.0/go.mod h1:qyLTv666WJ6ImFPUjljux+684Y/GGYUjAZcKCnc6yBs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= @@ -197,22 +461,26 @@ golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.284.0 h1:i+cKTgeQRcRySkP7QTl5PDO7/pAm8EcMFIUMlNbk4Vc= +google.golang.org/api v0.284.0/go.mod h1:AU44fU+XVZOCcd8uLaBIa/ZgzgPf/0qqY3+m7lQaado= +google.golang.org/genproto v0.0.0-20260608224507-4308a22a1bab h1:bG8JpL3dfsvJKRgrh7yMkswdxzBqQDRYqkLDHo3+708= +google.golang.org/genproto v0.0.0-20260608224507-4308a22a1bab/go.mod h1:cVHIikDNAdx8ISZeW+2rYkEMf3xn0GSaBYmVnWXQBUo= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= @@ -222,12 +490,15 @@ google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zN google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -265,3 +536,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80 sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= +software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/kagenti-operator/internal/agentcard/fetcher.go b/kagenti-operator/internal/agentcard/fetcher.go index b1fc8290..390f145d 100644 --- a/kagenti-operator/internal/agentcard/fetcher.go +++ b/kagenti-operator/internal/agentcard/fetcher.go @@ -51,7 +51,10 @@ const ( const ( SignedCardConfigMapSuffix = "-card-signed" - SignedCardConfigMapKey = "agent-card.json" + // SignedAgentCardConfigMapKey is the preferred ConfigMap key for sigstore-a2a SignedAgentCard JSON. + SignedAgentCardConfigMapKey = "signed-agent-card.json" + // SignedCardLegacyConfigMapKey holds a plain A2A agent card or legacy layouts. + SignedCardLegacyConfigMapKey = "agent-card.json" ) // ConfigMapName returns the expected ConfigMap name for a signed agent card. @@ -59,9 +62,18 @@ func ConfigMapName(agentName string) string { return agentName + SignedCardConfigMapSuffix } +// FetchResult is the outcome of fetching agent card content from ConfigMap and/or HTTP. +type FetchResult struct { + CardData *agentv1alpha1.AgentCardData + AgentSpiffeID string + // RawSignedAgentCardJSON is non-nil when the source document was a SignedAgentCard + // (agentCard + attestations). Used for Sigstore bundle verification. + RawSignedAgentCardJSON []byte +} + type Fetcher interface { Fetch(ctx context.Context, protocol, serviceURL, agentName, namespace string, - ) (*agentv1alpha1.AgentCardData, error) + ) (*FetchResult, error) } type DefaultFetcher struct { @@ -78,7 +90,7 @@ func NewFetcher() Fetcher { func (f *DefaultFetcher) Fetch( ctx context.Context, protocol, serviceURL, _, _ string, -) (*agentv1alpha1.AgentCardData, error) { +) (*FetchResult, error) { switch protocol { case A2AProtocol: return f.fetchA2ACard(ctx, serviceURL) @@ -87,10 +99,10 @@ func (f *DefaultFetcher) Fetch( } } -func (f *DefaultFetcher) fetchA2ACard(ctx context.Context, serviceURL string) (*agentv1alpha1.AgentCardData, error) { - card, err := f.fetchAgentCardFromPath(ctx, serviceURL, A2AAgentCardPath) +func (f *DefaultFetcher) fetchA2ACard(ctx context.Context, serviceURL string) (*FetchResult, error) { + res, err := f.fetchAgentCardFromPath(ctx, serviceURL, A2AAgentCardPath) if err == nil { - return card, nil + return res, nil } if !errors.Is(err, errNotFound) { @@ -101,7 +113,7 @@ func (f *DefaultFetcher) fetchA2ACard(ctx context.Context, serviceURL string) (* "currentPath", A2AAgentCardPath, "legacyPath", A2ALegacyAgentCardPath) - card, legacyErr := f.fetchAgentCardFromPath(ctx, serviceURL, A2ALegacyAgentCardPath) + res, legacyErr := f.fetchAgentCardFromPath(ctx, serviceURL, A2ALegacyAgentCardPath) if legacyErr != nil { return nil, legacyErr } @@ -110,9 +122,9 @@ func (f *DefaultFetcher) fetchA2ACard(ctx context.Context, serviceURL string) (* "deprecated", true, "migrateTo", A2AAgentCardPath, "legacyPath", A2ALegacyAgentCardPath, - "agentName", card.Name) + "agentName", res.CardData.Name) - return card, nil + return res, nil } // errNotFound is returned when the agent card endpoint returns HTTP 404. @@ -156,7 +168,7 @@ func doHTTPFetch(ctx context.Context, httpClient *http.Client, fetchURL string) func (f *DefaultFetcher) fetchAgentCardFromPath( ctx context.Context, serviceURL, path string, -) (*agentv1alpha1.AgentCardData, error) { +) (*FetchResult, error) { agentCardURL := serviceURL + path fetcherLogger.Info("Fetching A2A agent card", "url", agentCardURL) @@ -165,6 +177,30 @@ func (f *DefaultFetcher) fetchAgentCardFromPath( return nil, err } + return decodeAgentCardPayload(body) +} + +func decodeAgentCardPayload(body []byte) (*FetchResult, error) { + var envelopeProbe struct { + AgentCard json.RawMessage `json:"agentCard"` + Attestations json.RawMessage `json:"attestations"` + VerificationMaterial json.RawMessage `json:"verificationMaterial"` + } + if err := json.Unmarshal(body, &envelopeProbe); err != nil { + return nil, fmt.Errorf("failed to parse agent card JSON: %w", err) + } + hasAtt := len(envelopeProbe.Attestations) > 0 || len(envelopeProbe.VerificationMaterial) > 0 + if len(envelopeProbe.AgentCard) > 0 && hasAtt { + var inner agentv1alpha1.AgentCardData + if err := json.Unmarshal(envelopeProbe.AgentCard, &inner); err != nil { + return nil, fmt.Errorf("failed to parse embedded agentCard: %w", err) + } + fetcherLogger.Info("Fetched SignedAgentCard envelope over HTTP", + "name", inner.Name, + "version", inner.Version) + return &FetchResult{CardData: &inner, RawSignedAgentCardJSON: append([]byte(nil), body...)}, nil + } + var agentCardData agentv1alpha1.AgentCardData if err := json.Unmarshal(body, &agentCardData); err != nil { return nil, fmt.Errorf("failed to parse agent card JSON: %w", err) @@ -175,7 +211,7 @@ func (f *DefaultFetcher) fetchAgentCardFromPath( "version", agentCardData.Version, "url", agentCardData.URL) - return &agentCardData, nil + return &FetchResult{CardData: &agentCardData}, nil } // ConfigMapFetcher reads signed agent cards from a ConfigMap before falling @@ -195,18 +231,27 @@ func NewConfigMapFetcher(reader client.Reader) Fetcher { func (f *ConfigMapFetcher) Fetch( ctx context.Context, protocol, serviceURL, agentName, namespace string, -) (*agentv1alpha1.AgentCardData, error) { +) (*FetchResult, error) { if agentName != "" && namespace != "" { cmName := agentName + SignedCardConfigMapSuffix var cm corev1.ConfigMap err := f.reader.Get(ctx, types.NamespacedName{Name: cmName, Namespace: namespace}, &cm) if err == nil { - if cardJSON, ok := cm.Data[SignedCardConfigMapKey]; ok { - var cardData agentv1alpha1.AgentCardData - if jsonErr := json.Unmarshal([]byte(cardJSON), &cardData); jsonErr == nil { - fetcherLogger.Info("Fetched signed agent card from ConfigMap", - "configMap", cmName, "namespace", namespace, "agentName", cardData.Name) - return &cardData, nil + var raw string + var keyUsed string + if s, ok := cm.Data[SignedAgentCardConfigMapKey]; ok { + raw = s + keyUsed = SignedAgentCardConfigMapKey + } else if s, ok := cm.Data[SignedCardLegacyConfigMapKey]; ok { + raw = s + keyUsed = SignedCardLegacyConfigMapKey + } + if raw != "" { + result, decErr := decodeAgentCardPayload([]byte(raw)) + if decErr == nil { + fetcherLogger.Info("Fetched agent card from ConfigMap", + "configMap", cmName, "namespace", namespace, "key", keyUsed, "agentName", result.CardData.Name) + return result, nil } fetcherLogger.Info("ConfigMap contains invalid JSON, falling back to HTTP", "configMap", cmName, "namespace", namespace) @@ -229,13 +274,6 @@ func GetSecureServiceURL(agentName, namespace string, port int32) string { return fmt.Sprintf("https://%s.%s.svc.cluster.local:%d", agentName, namespace, port) } -// FetchResult contains the result of an authenticated fetch including -// the agent's verified SPIFFE ID extracted from the TLS peer certificate. -type FetchResult struct { - CardData *agentv1alpha1.AgentCardData - AgentSpiffeID string -} - // AuthenticatedFetcher performs mTLS-authenticated fetches and returns // identity information from the TLS handshake alongside the card data. type AuthenticatedFetcher interface { @@ -319,22 +357,20 @@ func (f *SpiffeFetcher) fetchAuthenticatedFromPath(ctx context.Context, serviceU return nil, err } - var agentCardData agentv1alpha1.AgentCardData - if err := json.Unmarshal(body, &agentCardData); err != nil { - return nil, fmt.Errorf("failed to parse agent card JSON: %w", err) + result, err := decodeAgentCardPayload(body) + if err != nil { + return nil, err } agentSpiffeID := extractSpiffeIDFromTLS(tlsState) + result.AgentSpiffeID = agentSpiffeID fetcherLogger.Info("Successfully fetched agent card (mTLS)", - "name", agentCardData.Name, - "version", agentCardData.Version, + "name", result.CardData.Name, + "version", result.CardData.Version, "agentSpiffeID", agentSpiffeID) - return &FetchResult{ - CardData: &agentCardData, - AgentSpiffeID: agentSpiffeID, - }, nil + return result, nil } // extractSpiffeIDFromTLS returns the SPIFFE ID from the verified peer diff --git a/kagenti-operator/internal/agentcard/fetcher_test.go b/kagenti-operator/internal/agentcard/fetcher_test.go index f14122ff..b3b159fd 100644 --- a/kagenti-operator/internal/agentcard/fetcher_test.go +++ b/kagenti-operator/internal/agentcard/fetcher_test.go @@ -45,8 +45,8 @@ func TestDefaultFetcher_SuccessfulA2ACardFetch(t *testing.T) { result, err := NewFetcher().Fetch(context.Background(), A2AProtocol, server.URL, "", "") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(result.Name).To(Equal("test-agent")) - g.Expect(result.Version).To(Equal("1.0")) + g.Expect(result.CardData.Name).To(Equal("test-agent")) + g.Expect(result.CardData.Version).To(Equal("1.0")) } func TestFetchA2ACard_LegacyFallback(t *testing.T) { @@ -61,9 +61,9 @@ func TestFetchA2ACard_LegacyFallback(t *testing.T) { })) defer srv.Close() - card, err := NewFetcher().Fetch(context.Background(), A2AProtocol, srv.URL, "", "") + result, err := NewFetcher().Fetch(context.Background(), A2AProtocol, srv.URL, "", "") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(card.Name).To(Equal("test-agent")) + g.Expect(result.CardData.Name).To(Equal("test-agent")) } func TestFetchA2ACard_BothNotFound(t *testing.T) { @@ -140,9 +140,9 @@ func TestFetchA2ACard_WithProviderField(t *testing.T) { result, err := NewFetcher().Fetch(context.Background(), A2AProtocol, server.URL, "", "") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(result.Provider).NotTo(BeNil()) - g.Expect(result.Provider.Organization).To(Equal("ACME Corp")) - g.Expect(result.Provider.URL).To(Equal("https://acme.example.com")) + g.Expect(result.CardData.Provider).NotTo(BeNil()) + g.Expect(result.CardData.Provider.Organization).To(Equal("ACME Corp")) + g.Expect(result.CardData.Provider.URL).To(Equal("https://acme.example.com")) } func TestFetchA2ACard_WithDocAndIconURLs(t *testing.T) { @@ -162,8 +162,8 @@ func TestFetchA2ACard_WithDocAndIconURLs(t *testing.T) { result, err := NewFetcher().Fetch(context.Background(), A2AProtocol, server.URL, "", "") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(result.DocumentationURL).To(Equal("https://docs.example.com")) - g.Expect(result.IconURL).To(Equal("https://example.com/icon.png")) + g.Expect(result.CardData.DocumentationURL).To(Equal("https://docs.example.com")) + g.Expect(result.CardData.IconURL).To(Equal("https://example.com/icon.png")) } func TestFetchA2ACard_WithExtensions(t *testing.T) { @@ -192,9 +192,9 @@ func TestFetchA2ACard_WithExtensions(t *testing.T) { result, err := NewFetcher().Fetch(context.Background(), A2AProtocol, server.URL, "", "") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(result.Capabilities).NotTo(BeNil()) - g.Expect(result.Capabilities.Extensions).To(HaveLen(1)) - ext := result.Capabilities.Extensions[0] + g.Expect(result.CardData.Capabilities).NotTo(BeNil()) + g.Expect(result.CardData.Capabilities.Extensions).To(HaveLen(1)) + ext := result.CardData.Capabilities.Extensions[0] g.Expect(ext.URI).To(Equal("urn:ext:logging")) g.Expect(ext.Description).To(Equal("Logging extension")) g.Expect(*ext.Required).To(BeTrue()) @@ -250,44 +250,44 @@ func TestFetchA2ACard_FullA2ACompatibility(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) // Core fields - g.Expect(result.Name).To(Equal("full-agent")) - g.Expect(result.Description).To(Equal("A fully compatible A2A agent")) - g.Expect(result.Version).To(Equal("3.0")) - g.Expect(result.URL).To(Equal("http://example.com/agent")) + g.Expect(result.CardData.Name).To(Equal("full-agent")) + g.Expect(result.CardData.Description).To(Equal("A fully compatible A2A agent")) + g.Expect(result.CardData.Version).To(Equal("3.0")) + g.Expect(result.CardData.URL).To(Equal("http://example.com/agent")) // New fields - g.Expect(result.DocumentationURL).To(Equal("https://docs.example.com/full-agent")) - g.Expect(result.IconURL).To(Equal("https://example.com/full-agent/icon.png")) + g.Expect(result.CardData.DocumentationURL).To(Equal("https://docs.example.com/full-agent")) + g.Expect(result.CardData.IconURL).To(Equal("https://example.com/full-agent/icon.png")) // Provider - g.Expect(result.Provider).NotTo(BeNil()) - g.Expect(result.Provider.Organization).To(Equal("Full Corp")) - g.Expect(result.Provider.URL).To(Equal("https://fullcorp.example.com")) + g.Expect(result.CardData.Provider).NotTo(BeNil()) + g.Expect(result.CardData.Provider.Organization).To(Equal("Full Corp")) + g.Expect(result.CardData.Provider.URL).To(Equal("https://fullcorp.example.com")) // Capabilities + extensions - g.Expect(result.Capabilities).NotTo(BeNil()) - g.Expect(*result.Capabilities.Streaming).To(BeTrue()) - g.Expect(*result.Capabilities.PushNotifications).To(BeFalse()) - g.Expect(result.Capabilities.Extensions).To(HaveLen(2)) + g.Expect(result.CardData.Capabilities).NotTo(BeNil()) + g.Expect(*result.CardData.Capabilities.Streaming).To(BeTrue()) + g.Expect(*result.CardData.Capabilities.PushNotifications).To(BeFalse()) + g.Expect(result.CardData.Capabilities.Extensions).To(HaveLen(2)) - audit := result.Capabilities.Extensions[0] + audit := result.CardData.Capabilities.Extensions[0] g.Expect(audit.URI).To(Equal("urn:ext:audit")) g.Expect(audit.Description).To(Equal("Audit trail")) g.Expect(*audit.Required).To(BeFalse()) g.Expect(audit.Params).To(HaveKeyWithValue("retention", apiextensionsv1.JSON{Raw: json.RawMessage(`"30d"`)})) - metrics := result.Capabilities.Extensions[1] + metrics := result.CardData.Capabilities.Extensions[1] g.Expect(metrics.URI).To(Equal("urn:ext:metrics")) g.Expect(metrics.Description).To(Equal("Metrics collection")) g.Expect(metrics.Required).To(BeNil()) g.Expect(metrics.Params).To(BeEmpty()) // Existing fields still work - g.Expect(result.DefaultInputModes).To(Equal([]string{"text", "application/json"})) - g.Expect(result.DefaultOutputModes).To(Equal([]string{"text"})) - g.Expect(result.Skills).To(HaveLen(1)) - g.Expect(result.Skills[0].Name).To(Equal("summarize")) - g.Expect(*result.SupportsAuthenticatedExtendedCard).To(BeTrue()) + g.Expect(result.CardData.DefaultInputModes).To(Equal([]string{"text", "application/json"})) + g.Expect(result.CardData.DefaultOutputModes).To(Equal([]string{"text"})) + g.Expect(result.CardData.Skills).To(HaveLen(1)) + g.Expect(result.CardData.Skills[0].Name).To(Equal("summarize")) + g.Expect(*result.CardData.SupportsAuthenticatedExtendedCard).To(BeTrue()) } func TestConfigMapFetcher_ConfigMapFound(t *testing.T) { @@ -298,7 +298,7 @@ func TestConfigMapFetcher_ConfigMapFound(t *testing.T) { Namespace: "test-ns", }, Data: map[string]string{ - SignedCardConfigMapKey: testAgentCardJSON, + SignedCardLegacyConfigMapKey: testAgentCardJSON, }, } @@ -308,10 +308,10 @@ func TestConfigMapFetcher_ConfigMapFound(t *testing.T) { Build() fetcher := NewConfigMapFetcher(fakeClient) - card, err := fetcher.Fetch(context.Background(), A2AProtocol, "", "my-agent", "test-ns") + res, err := fetcher.Fetch(context.Background(), A2AProtocol, "", "my-agent", "test-ns") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(card.Name).To(Equal("test-agent")) - g.Expect(card.Version).To(Equal("1.0")) + g.Expect(res.CardData.Name).To(Equal("test-agent")) + g.Expect(res.CardData.Version).To(Equal("1.0")) } func TestConfigMapFetcher_ConfigMapNotFound(t *testing.T) { @@ -331,9 +331,9 @@ func TestConfigMapFetcher_ConfigMapNotFound(t *testing.T) { Build() fetcher := NewConfigMapFetcher(fakeClient) - card, err := fetcher.Fetch(context.Background(), A2AProtocol, srv.URL, "no-such-agent", "test-ns") + res, err := fetcher.Fetch(context.Background(), A2AProtocol, srv.URL, "no-such-agent", "test-ns") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(card.Name).To(Equal("test-agent")) + g.Expect(res.CardData.Name).To(Equal("test-agent")) } func TestConfigMapFetcher_MissingKey(t *testing.T) { @@ -362,9 +362,9 @@ func TestConfigMapFetcher_MissingKey(t *testing.T) { Build() fetcher := NewConfigMapFetcher(fakeClient) - card, err := fetcher.Fetch(context.Background(), A2AProtocol, srv.URL, "empty-agent", "test-ns") + res, err := fetcher.Fetch(context.Background(), A2AProtocol, srv.URL, "empty-agent", "test-ns") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(card.Name).To(Equal("test-agent")) + g.Expect(res.CardData.Name).To(Equal("test-agent")) } func TestConfigMapFetcher_InvalidJSON(t *testing.T) { @@ -375,7 +375,7 @@ func TestConfigMapFetcher_InvalidJSON(t *testing.T) { Namespace: "test-ns", }, Data: map[string]string{ - SignedCardConfigMapKey: "not valid json{{{", + SignedCardLegacyConfigMapKey: "not valid json{{{", }, } @@ -395,7 +395,7 @@ func TestConfigMapFetcher_InvalidJSON(t *testing.T) { Build() fetcher := NewConfigMapFetcher(fakeClient) - card, err := fetcher.Fetch(context.Background(), A2AProtocol, srv.URL, "bad-agent", "test-ns") + res, err := fetcher.Fetch(context.Background(), A2AProtocol, srv.URL, "bad-agent", "test-ns") g.Expect(err).NotTo(HaveOccurred()) - g.Expect(card.Name).To(Equal("test-agent")) + g.Expect(res.CardData.Name).To(Equal("test-agent")) } diff --git a/kagenti-operator/internal/controller/agentcard_controller.go b/kagenti-operator/internal/controller/agentcard_controller.go index 24f392c3..dfaa5e7c 100644 --- a/kagenti-operator/internal/controller/agentcard_controller.go +++ b/kagenti-operator/internal/controller/agentcard_controller.go @@ -35,7 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" + "k8s.io/client-go/tools/events" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -95,6 +95,11 @@ const ( ReasonSignatureValid = "SignatureValid" ReasonSignatureInvalid = "SignatureInvalid" ReasonSignatureInvalidAudit = "SignatureInvalidAudit" + + ReasonSigstoreVerified = "SigstoreVerified" + ReasonSigstoreInvalid = "SigstoreVerificationFailed" + ReasonSigstoreInvalidAudit = "SigstoreVerificationFailedAudit" + ReasonSigstoreBundleMissing = "SigstoreBundleNotFound" ) var ( @@ -117,7 +122,7 @@ type WorkloadInfo struct { type AgentCardReconciler struct { client.Client Scheme *runtime.Scheme - Recorder record.EventRecorder + Recorder events.EventRecorder AgentFetcher agentcard.Fetcher @@ -132,6 +137,10 @@ type AgentCardReconciler struct { RequireSignature bool SignatureAuditMode bool + BundleVerifier signature.BundleVerifier + EnableSigstoreVerification bool + SigstoreAuditMode bool + // SpireTrustDomain can be overridden per-AgentCard via spec.identityBinding.trustDomain. SpireTrustDomain string @@ -148,6 +157,7 @@ type AgentCardReconciler struct { // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch // +kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes,verbs=get;list;watch +// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch func (r *AgentCardReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { agentCardLogger.V(1).Info("Reconciling AgentCard", "namespacedName", req.NamespacedName) @@ -175,7 +185,7 @@ func (r *AgentCardReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( agentCardLogger.Info("AgentCard is deprecated; card data is now available via AgentRuntime status.card. Migrate to AgentRuntime-based discovery.", "agentCard", agentCard.Name, "namespace", agentCard.Namespace) if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeWarning, "Deprecated", + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "Deprecated", "Reconcile", "AgentCard is deprecated; card data is now available via AgentRuntime status.card. Migrate to AgentRuntime-based discovery.") } } @@ -206,7 +216,7 @@ func (r *AgentCardReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, bindErr } if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeWarning, ReasonAgentNotFound, message) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, ReasonAgentNotFound, "FetchAgentCard", message) } } return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil @@ -242,6 +252,7 @@ func (r *AgentCardReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( servicePort := r.getServicePort(service) serviceURL := agentcard.GetServiceURL(workload.ServiceName, agentCard.Namespace, servicePort) + // Use fetchCardData which handles both mTLS authenticated fetch and regular HTTP fetch cardData, fetchResult, err := r.fetchCardData(ctx, agentCard, protocol, serviceURL, workload, service) if err != nil { if condErr := r.updateCondition(ctx, agentCard, @@ -251,15 +262,59 @@ func (r *AgentCardReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil } + var sigstoreResult *signature.BundleVerificationResult + if r.EnableSigstoreVerification && r.BundleVerifier != nil { + if len(fetchResult.RawSignedAgentCardJSON) > 0 { + var sErr error + sigstoreResult, sErr = r.BundleVerifier.VerifySignedAgentCard(ctx, fetchResult.RawSignedAgentCardJSON, agentCard.Spec.SigstoreVerification) + if sErr != nil { + agentCardLogger.Error(sErr, "Sigstore bundle verification error", "workload", workload.Name) + // M-3: Set failed result so enforcement mode can reject the card + sigstoreResult = &signature.BundleVerificationResult{ + Verified: false, + Details: fmt.Sprintf("infrastructure error: %s", sErr.Error()), + } + if r.Recorder != nil { + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "SigstoreVerificationFailed", + "VerifySigstore", "Infrastructure error during Sigstore verification: %s", sErr.Error()) + } + } else if sigstoreResult != nil { + // P-3: Emit events for Sigstore verification + if sigstoreResult.Verified { + if r.Recorder != nil { + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeNormal, "SigstoreVerified", + "VerifySigstore", "Sigstore bundle verified successfully (identity=%s, rekorLogIndex=%s)", + sigstoreResult.Identity, sigstoreResult.RekorLogIndex) + } + } else if !sigstoreResult.Absent { + if r.Recorder != nil { + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "SigstoreVerificationFailed", + "VerifySigstore", sigstoreResult.Details) + } + } + } + } else { + sigstoreResult = &signature.BundleVerificationResult{ + Absent: true, + Verified: false, + Details: "only plain agent card available (no SignedAgentCard attestations); supply-chain bundle not present", + } + if r.Recorder != nil { + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "SigstoreBundleNotFound", + "VerifySigstore", "No Sigstore bundle found - plain agent card without attestations") + } + } + } + trust := r.evaluateTrust(ctx, agentCard, cardData, fetchResult, workload) cardData.URL = serviceURL cardID := r.computeCardID(cardData) if cardID != "" && agentCard.Status.CardId != "" && agentCard.Status.CardId != cardID { if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeWarning, "CardContentChanged", - fmt.Sprintf("Agent card content changed: previous=%s, current=%s", - agentCard.Status.CardId, cardID)) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "CardContentChanged", "Reconcile", + "Agent card content changed: previous=%s, current=%s", + agentCard.Status.CardId, cardID) } agentCardLogger.Info("Card content changed", "agentCard", agentCard.Name, @@ -276,7 +331,7 @@ func (r *AgentCardReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( if err := r.updateAgentCardStatus(ctx, agentCard, cardData, protocol, cardID, resolvedTargetRef, trust.verificationResult, trust.binding, trust.identityMatch, trust.isMTLSVerified, trust.verifiedReason, trust.verifiedStatus, - trust.verifiedMessage, trust.attestedSpiffeID); err != nil { + trust.verifiedMessage, trust.attestedSpiffeID, sigstoreResult); err != nil { agentCardLogger.Error(err, "Failed to update AgentCard status") return ctrl.Result{}, err } @@ -359,11 +414,11 @@ func (r *AgentCardReconciler) fetchCardData( agentCardLogger.Info("TLS port not found on service, falling back to HTTP fetch", "service", workload.ServiceName, "expectedPortName", AgentTLSPortName) if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeWarning, "FallbackToHTTP", - fmt.Sprintf("Service %s has no %s port; fetch is unverified", - workload.ServiceName, AgentTLSPortName)) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "FallbackToHTTP", "FetchCard", + "Service %s has no %s port; fetch is unverified", + workload.ServiceName, AgentTLSPortName) } - cardData, err := r.AgentFetcher.Fetch( + fetchResult, err := r.AgentFetcher.Fetch( ctx, protocol, serviceURL, workload.ServiceName, agentCard.Namespace) if err != nil { agentCardLogger.Error(err, "Failed to fetch agent card", @@ -374,10 +429,10 @@ func (r *AgentCardReconciler) fetchCardData( } return nil, nil, err } - return cardData, nil, nil + return fetchResult.CardData, fetchResult, nil } - cardData, err := r.AgentFetcher.Fetch( + fetchResult, err := r.AgentFetcher.Fetch( ctx, protocol, serviceURL, workload.ServiceName, agentCard.Namespace) if err != nil { agentCardLogger.Error(err, "Failed to fetch agent card", @@ -391,7 +446,7 @@ func (r *AgentCardReconciler) fetchCardData( if err := r.cleanupVerifiedFetchFields(ctx, agentCard); err != nil { agentCardLogger.Error(err, "Failed to cleanup verified fetch fields", "agentCard", agentCard.Name) } - return cardData, nil, nil + return fetchResult.CardData, fetchResult, nil } // evaluateTrust performs signature verification, binding computation, and @@ -412,8 +467,8 @@ func (r *AgentCardReconciler) evaluateTrust( //nolint:gocyclo if eval.verificationResult != nil { if eval.verificationResult.Verified { if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeNormal, "SignatureEvaluated", - fmt.Sprintf("Signature verified successfully (keyID=%s)", eval.verificationResult.KeyID)) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeNormal, "SignatureEvaluated", + "VerifySignature", "Signature verified successfully (keyID=%s)", eval.verificationResult.KeyID) } } else { reason := ReasonSignatureInvalid @@ -425,7 +480,8 @@ func (r *AgentCardReconciler) evaluateTrust( //nolint:gocyclo "reason", reason, "details", eval.verificationResult.Details) if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeWarning, "SignatureFailed", eval.verificationResult.Details) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "SignatureFailed", + "VerifySignature", eval.verificationResult.Details) } } } @@ -722,16 +778,20 @@ func (r *AgentCardReconciler) getSyncPeriod(agentCard *agentv1alpha1.AgentCard) // Absent when no trust mechanism is configured. // - SignatureVerified: raw JWS cryptographic outcome (informational only, not used // for enforcement). Reflects whether the x5c signature is mathematically valid. +// - SigstoreVerified: supply-chain verification via Sigstore bundle attestations. // - Synced: whether the agent card was successfully fetched. // - Ready: composite signal — True when Synced is True AND (Verified is True or absent). // - Bound: whether identity binding constraints are satisfied. -func (r *AgentCardReconciler) updateAgentCardStatus( //nolint:gocyclo +// +//nolint:gocyclo // TODO: refactor to reduce complexity +func (r *AgentCardReconciler) updateAgentCardStatus( ctx context.Context, agentCard *agentv1alpha1.AgentCard, cardData *agentv1alpha1.AgentCardData, protocol, cardID string, targetRef *agentv1alpha1.TargetRef, verificationResult *signature.VerificationResult, binding *bindingResult, identityMatch *bool, mTLSVerified bool, verifiedReason string, verifiedStatus metav1.ConditionStatus, verifiedMessage string, attestedSpiffeID string, + sigstoreResult *signature.BundleVerificationResult, ) error { return retry.RetryOnConflict(retry.DefaultRetry, func() error { latest := &agentv1alpha1.AgentCard{} @@ -782,6 +842,62 @@ func (r *AgentCardReconciler) updateAgentCardStatus( //nolint:gocyclo meta.SetStatusCondition(&latest.Status.Conditions, sigCondition) } + // Populate Sigstore verification status fields + if r.EnableSigstoreVerification && sigstoreResult != nil { + if sigstoreResult.Absent { + absent := false + latest.Status.SigstoreBundleVerified = &absent + latest.Status.SigstoreIdentity = "" + latest.Status.RekorLogIndex = "" + latest.Status.SLSARepository = "" + latest.Status.SLSACommitSHA = "" + meta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{ + Type: "SigstoreVerified", + Status: metav1.ConditionFalse, + Reason: ReasonSigstoreBundleMissing, + Message: sigstoreResult.Details, + }) + } else { + latest.Status.SigstoreBundleVerified = &sigstoreResult.Verified + if sigstoreResult.Verified { + latest.Status.SigstoreIdentity = sigstoreResult.Identity + latest.Status.RekorLogIndex = sigstoreResult.RekorLogIndex + latest.Status.SLSARepository = sigstoreResult.SLSARepository + latest.Status.SLSACommitSHA = sigstoreResult.SLSACommitSHA + } else { + latest.Status.SigstoreIdentity = "" + latest.Status.RekorLogIndex = "" + latest.Status.SLSARepository = "" + latest.Status.SLSACommitSHA = "" + } + sigStoreCond := metav1.Condition{Type: "SigstoreVerified"} + if sigstoreResult.Verified { + sigStoreCond.Status = metav1.ConditionTrue + sigStoreCond.Reason = ReasonSigstoreVerified + sigStoreCond.Message = sigstoreResult.Details + } else { + sigStoreCond.Status = metav1.ConditionFalse + if r.SigstoreAuditMode { + sigStoreCond.Reason = ReasonSigstoreInvalidAudit + sigStoreCond.Message = sigstoreResult.Details + " (audit mode: allowed)" + } else { + sigStoreCond.Reason = ReasonSigstoreInvalid + sigStoreCond.Message = sigstoreResult.Details + } + } + meta.SetStatusCondition(&latest.Status.Conditions, sigStoreCond) + } + } else { + // Clear Sigstore status fields when verification is disabled + latest.Status.SigstoreBundleVerified = nil + latest.Status.SigstoreIdentity = "" + latest.Status.RekorLogIndex = "" + latest.Status.SLSARepository = "" + latest.Status.SLSACommitSHA = "" + meta.RemoveStatusCondition(&latest.Status.Conditions, "SigstoreVerified") + } + + // Set Synced condition to false if JWS verification failed (when not mTLS-verified and not audit mode) if verificationResult != nil && !verificationResult.Verified && !r.SignatureAuditMode && !mTLSVerified { meta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{ Type: "Synced", @@ -789,13 +905,25 @@ func (r *AgentCardReconciler) updateAgentCardStatus( //nolint:gocyclo Reason: ReasonSignatureInvalid, Message: verificationResult.Details, }) + // Check if Sigstore verification failed (when not in audit mode) + } else if sigstoreResult != nil && !sigstoreResult.Absent && !sigstoreResult.Verified && !r.SigstoreAuditMode { + meta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{ + Type: "Synced", + Status: metav1.ConditionFalse, + Reason: ReasonSigstoreInvalid, + Message: sigstoreResult.Details, + }) } else { + // Both verifications passed or are in audit mode - set Synced to True message := fmt.Sprintf("Successfully fetched agent card for %s", cardData.Name) if verificationResult != nil && !verificationResult.Verified && r.SignatureAuditMode { message = fmt.Sprintf("Fetched agent card for %s (signature verification failed but audit mode enabled)", cardData.Name) } else if mTLSVerified && verificationResult != nil && !verificationResult.Verified { message = fmt.Sprintf("Successfully fetched agent card for %s (mTLS verified, no JWS signature)", cardData.Name) } + if sigstoreResult != nil && !sigstoreResult.Absent && !sigstoreResult.Verified && r.SigstoreAuditMode { + message = fmt.Sprintf("%s (Sigstore bundle failed audit mode)", message) + } meta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{ Type: "Synced", Status: metav1.ConditionTrue, @@ -855,9 +983,11 @@ func (r *AgentCardReconciler) updateAgentCardStatus( //nolint:gocyclo if existingBound == nil || existingBound.Status != newConditionStatus { if r.Recorder != nil { if binding.Bound { - r.Recorder.Event(agentCard, corev1.EventTypeNormal, "BindingEvaluated", binding.Message) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeNormal, "BindingEvaluated", + "EvaluateBinding", binding.Message) } else { - r.Recorder.Event(agentCard, corev1.EventTypeWarning, "BindingFailed", binding.Message) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeWarning, "BindingFailed", + "EvaluateBinding", binding.Message) } } } @@ -1434,7 +1564,8 @@ func (r *AgentCardReconciler) maybeRestartForResign(ctx context.Context, agentCa agentCardLogger.Info("Triggering proactive workload restart for re-signing", "workload", workload.Name, "kind", workload.Kind, "reason", reason) if r.Recorder != nil { - r.Recorder.Event(agentCard, corev1.EventTypeNormal, "ResignTriggered", reason) + r.Recorder.Eventf(agentCard, nil, corev1.EventTypeNormal, "ResignTriggered", + "TriggerResign", reason) } r.triggerRolloutRestart(ctx, acc, key, currentBundleHash, vr.LeafNotAfter) diff --git a/kagenti-operator/internal/controller/agentcard_controller_test.go b/kagenti-operator/internal/controller/agentcard_controller_test.go index 52a079c0..85bcfc75 100644 --- a/kagenti-operator/internal/controller/agentcard_controller_test.go +++ b/kagenti-operator/internal/controller/agentcard_controller_test.go @@ -30,7 +30,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" + "k8s.io/client-go/tools/events" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -46,11 +46,11 @@ type mockFetcher struct { func (m *mockFetcher) Fetch( ctx context.Context, protocol, url, _, _ string, -) (*agentv1alpha1.AgentCardData, error) { +) (*agentcard.FetchResult, error) { if m.err != nil { return nil, m.err } - return m.cardData, nil + return &agentcard.FetchResult{CardData: m.cardData}, nil } // capturingProtocolFetcher records the protocol passed to Fetch (for multi-protocol label tests). @@ -62,18 +62,18 @@ type capturingProtocolFetcher struct { func (c *capturingProtocolFetcher) Fetch( ctx context.Context, protocol, _, _, _ string, -) (*agentv1alpha1.AgentCardData, error) { +) (*agentcard.FetchResult, error) { c.mu.Lock() c.protocol = protocol c.mu.Unlock() if c.cardData != nil { - return c.cardData, nil + return &agentcard.FetchResult{CardData: c.cardData}, nil } - return &agentv1alpha1.AgentCardData{ + return &agentcard.FetchResult{CardData: &agentv1alpha1.AgentCardData{ Name: "capture", Version: "1.0.0", URL: "http://0.0.0.0:8000", - }, nil + }}, nil } func (c *capturingProtocolFetcher) lastProtocol() string { @@ -1278,7 +1278,7 @@ var _ = Describe("AgentCard Controller — ConfigMap-backed fetch", func() { Name: deploymentName + agentcard.SignedCardConfigMapSuffix, Namespace: namespace, }, - Data: map[string]string{agentcard.SignedCardConfigMapKey: string(raw)}, + Data: map[string]string{agentcard.SignedCardLegacyConfigMapKey: string(raw)}, } Expect(k8sClient.Create(ctx, cm)).To(Succeed()) @@ -1807,7 +1807,7 @@ var _ = Describe("AgentCard Controller - getSyncPeriod", func() { URL: "http://deprecation-deploy.default.svc.cluster.local:8000", }, } - fakeRecorder := record.NewFakeRecorder(10) + fakeRecorder := events.NewFakeRecorder(10) reconciler := &AgentCardReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), diff --git a/kagenti-operator/internal/controller/agentruntime_controller.go b/kagenti-operator/internal/controller/agentruntime_controller.go index 0c4070dd..f435aa64 100644 --- a/kagenti-operator/internal/controller/agentruntime_controller.go +++ b/kagenti-operator/internal/controller/agentruntime_controller.go @@ -38,7 +38,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/record" + "k8s.io/client-go/tools/events" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -94,7 +94,7 @@ var sandboxGVK = schema.GroupVersionKind{ type AgentRuntimeReconciler struct { client.Client Scheme *runtime.Scheme - Recorder record.EventRecorder + Recorder events.EventRecorder APIReader client.Reader // uncached reader for cross-namespace ConfigMap reads AgentFetcher agentcard.Fetcher @@ -122,7 +122,7 @@ func (r *AgentRuntimeReconciler) getFeatureGates() *webhookconfig.FeatureGates { // +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch -// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=operator.openshift.io,resources=networks,verbs=get func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -157,7 +157,8 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request logger.Error(err, "Failed to resolve targetRef") r.updateErrorStatus(ctx, req.NamespacedName, ConditionTypeTargetResolved, "TargetNotFound", err.Error()) if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeWarning, "TargetNotFound", err.Error()) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "TargetNotFound", + "ResolveTarget", err.Error()) } return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } @@ -178,7 +179,8 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request if err := r.ensureNamespaceConfigMaps(ctx, rt.Namespace); err != nil { logger.Error(err, "Failed to ensure namespace ConfigMaps") if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeWarning, "ConfigMapEnsureError", err.Error()) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "ConfigMapEnsureError", + "EnsureConfigMaps", err.Error()) } } @@ -189,14 +191,15 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request logger.Error(istioErr, "Failed to ensure Istio mesh labels") r.setCondition(rt, ConditionTypeIstioMeshEnrolled, metav1.ConditionFalse, "PatchFailed", istioErr.Error()) if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeWarning, "IstioMeshLabelError", istioErr.Error()) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "IstioMeshLabelError", + "EnsureIstioMesh", istioErr.Error()) } case istioLabeled: r.setCondition(rt, ConditionTypeIstioMeshEnrolled, metav1.ConditionTrue, "NamespaceLabeled", fmt.Sprintf("Namespace %s enrolled in Istio ambient mesh", rt.Namespace)) if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeNormal, "IstioMeshEnrolled", - fmt.Sprintf("Namespace %s labeled for Istio ambient mesh", rt.Namespace)) + r.Recorder.Eventf(rt, nil, corev1.EventTypeNormal, "IstioMeshEnrolled", + "EnsureIstioMesh", "Namespace %s labeled for Istio ambient mesh", rt.Namespace) } default: r.setCondition(rt, ConditionTypeIstioMeshEnrolled, metav1.ConditionFalse, "OptedOut", @@ -211,7 +214,8 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request if err := r.ensureNamespaceSCCBinding(ctx, rt.Namespace); err != nil { logger.Error(err, "Failed to ensure SCC RoleBinding") if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeWarning, "SCCBindingError", err.Error()) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "SCCBindingError", + "EnsureSCCBinding", err.Error()) } r.updateErrorStatus(ctx, req.NamespacedName, ConditionTypeReady, "SCCBindingError", err.Error()) return ctrl.Result{RequeueAfter: 30 * time.Second}, nil @@ -231,7 +235,8 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request strings.Join(configResult.Warnings, "; ")) if r.Recorder != nil { for _, w := range configResult.Warnings { - r.Recorder.Event(rt, corev1.EventTypeWarning, "ConfigWarning", w) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "ConfigWarning", + "ResolveConfig", w) } } } else { @@ -284,8 +289,8 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request } if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeNormal, "Configured", - fmt.Sprintf("Applied config to %s %s", rt.Spec.TargetRef.Kind, rt.Spec.TargetRef.Name)) + r.Recorder.Eventf(rt, nil, corev1.EventTypeNormal, "Configured", + "ApplyConfig", "Applied config to %s %s", rt.Spec.TargetRef.Kind, rt.Spec.TargetRef.Name) } return ctrl.Result{}, nil @@ -474,8 +479,8 @@ func (r *AgentRuntimeReconciler) completeSandboxRestart(ctx context.Context, rt } if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeNormal, "SandboxRestarted", - fmt.Sprintf("Sandbox %s restarted via scale 0→1", ref.Name)) + r.Recorder.Eventf(rt, nil, corev1.EventTypeNormal, "SandboxRestarted", + "RestartSandbox", "Sandbox %s restarted via scale 0→1", ref.Name) } return ctrl.Result{RequeueAfter: 5 * time.Second}, true, nil @@ -534,8 +539,8 @@ func (r *AgentRuntimeReconciler) readLinkedSkills(ctx context.Context, rt *agent if err := json.Unmarshal([]byte(raw), &skills); err != nil { logger.V(1).Info("Failed to parse kagenti.io/skills annotation", "error", err, "raw", raw) if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeWarning, "SkillAnnotationParseError", - fmt.Sprintf("Failed to parse kagenti.io/skills annotation: %v", err)) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "SkillAnnotationParseError", + "ReadLinkedSkills", "Failed to parse kagenti.io/skills annotation: %v", err) } return nil } @@ -912,8 +917,8 @@ func (r *AgentRuntimeReconciler) fetchCard( logger.Info("TLS port not found, falling back to HTTP fetch", "service", svc.Name, "expectedPortName", AgentTLSPortName) if r.Recorder != nil { - r.Recorder.Event(rt, corev1.EventTypeWarning, "FallbackToHTTP", - fmt.Sprintf("Service %s has no %s port; fetch is unverified", svc.Name, AgentTLSPortName)) + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "FallbackToHTTP", "FetchCard", + "Service %s has no %s port; fetch is unverified", svc.Name, AgentTLSPortName) } } @@ -922,14 +927,14 @@ func (r *AgentRuntimeReconciler) fetchCard( } serviceURL := agentcard.GetServiceURL(svc.Name, rt.Namespace, port) - cardData, err := r.AgentFetcher.Fetch(ctx, protocol, serviceURL, ref.Name, rt.Namespace) + fetchResult, err := r.AgentFetcher.Fetch(ctx, protocol, serviceURL, ref.Name, rt.Namespace) if err != nil { return nil, nil, "", fmt.Errorf("fetch failed for %s: %w", ref.Name, err) } - if cardData == nil { + if fetchResult.CardData == nil { return nil, nil, "", fmt.Errorf("fetch returned nil card data for %s", ref.Name) } - return cardData, nil, agentv1alpha1.TransportSecurityHTTP, nil + return fetchResult.CardData, fetchResult, agentv1alpha1.TransportSecurityHTTP, nil } // workloadChangeKey returns a string that changes when the workload's pod diff --git a/kagenti-operator/internal/controller/agentruntime_controller_test.go b/kagenti-operator/internal/controller/agentruntime_controller_test.go index 363e63ce..e654f3c1 100644 --- a/kagenti-operator/internal/controller/agentruntime_controller_test.go +++ b/kagenti-operator/internal/controller/agentruntime_controller_test.go @@ -66,8 +66,11 @@ type stubCardFetcher struct { err error } -func (f *stubCardFetcher) Fetch(_ context.Context, _, _, _, _ string) (*agentv1alpha1.AgentCardData, error) { - return f.card, f.err +func (f *stubCardFetcher) Fetch(_ context.Context, _, _, _, _ string) (*agentcard.FetchResult, error) { + if f.err != nil { + return nil, f.err + } + return &agentcard.FetchResult{CardData: f.card}, nil } type stubAuthenticatedFetcher struct { diff --git a/kagenti-operator/internal/controller/mlflow_controller.go b/kagenti-operator/internal/controller/mlflow_controller.go index 42acc09b..c796cd83 100644 --- a/kagenti-operator/internal/controller/mlflow_controller.go +++ b/kagenti-operator/internal/controller/mlflow_controller.go @@ -28,7 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" + "k8s.io/client-go/tools/events" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -65,7 +65,7 @@ const ( type MLflowReconciler struct { client.Client Scheme *runtime.Scheme - Recorder record.EventRecorder + Recorder events.EventRecorder // MLflowCAFile is the path to a PEM-encoded CA bundle for verifying the // MLflow gateway TLS certificate. When set, the CA is appended to the @@ -87,6 +87,7 @@ type MLflowReconciler struct { // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;get;list;watch;update // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;get;list;watch;update // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch func (r *MLflowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) @@ -128,8 +129,8 @@ func (r *MLflowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err != nil { logger.Error(err, "Failed to create/get MLflow experiment", "name", experimentName) if r.Recorder != nil { - r.Recorder.Eventf(dep, "Warning", "MLflowExperimentFailed", - "Failed to create MLflow experiment %q: %v", experimentName, err) + r.Recorder.Eventf(dep, nil, corev1.EventTypeWarning, "MLflowExperimentFailed", + "CreateExperiment", "Failed to create MLflow experiment %q: %v", experimentName, err) } return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } @@ -153,8 +154,8 @@ func (r *MLflowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } if r.Recorder != nil { - r.Recorder.Eventf(dep, "Normal", "MLflowConfigured", - "Experiment %q (ID: %s) provisioned, RoleBinding created for SA %s", + r.Recorder.Eventf(dep, nil, corev1.EventTypeNormal, "MLflowConfigured", + "ConfigureMLflow", "Experiment %q (ID: %s) provisioned, RoleBinding created for SA %s", experimentName, experimentID, saName) } diff --git a/kagenti-operator/internal/controller/signature_verification_test.go b/kagenti-operator/internal/controller/signature_verification_test.go index 02523e1b..0c0433bc 100644 --- a/kagenti-operator/internal/controller/signature_verification_test.go +++ b/kagenti-operator/internal/controller/signature_verification_test.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" + "github.com/kagenti/operator/internal/agentcard" "github.com/kagenti/operator/internal/signature" ) @@ -1097,8 +1098,8 @@ type mockFetcherFunc struct { func (m *mockFetcherFunc) Fetch( _ context.Context, _, _, _, _ string, -) (*agentv1alpha1.AgentCardData, error) { - return m.fn(), nil +) (*agentcard.FetchResult, error) { + return &agentcard.FetchResult{CardData: m.fn()}, nil } // mockSignatureProvider wraps VerifyJWS with a fixed public key for tests. diff --git a/kagenti-operator/internal/signature/metrics.go b/kagenti-operator/internal/signature/metrics.go index 07747637..3916c8b3 100644 --- a/kagenti-operator/internal/signature/metrics.go +++ b/kagenti-operator/internal/signature/metrics.go @@ -46,6 +46,38 @@ var ( }, []string{"provider", "error_type"}, ) + + SigstoreVerificationTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "kagenti_sigstore_verification_total", + Help: "Sigstore SignedAgentCard bundle verification attempts", + }, + []string{"result", "reason"}, + ) + + SigstoreVerificationDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "kagenti_sigstore_verification_duration_seconds", + Help: "Sigstore bundle verification latency", + Buckets: prometheus.DefBuckets, + }, + []string{"provider"}, + ) + + SigstoreTrustedRootAgeSeconds = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "kagenti_sigstore_trusted_root_age_seconds", + Help: "Age of the cached Sigstore trusted root material", + }, + ) + + SLSAProvenanceVerifiedTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "kagenti_slsa_provenance_verified_total", + Help: "SLSA provenance bundle observations after Sigstore verify", + }, + []string{"result"}, + ) ) func init() { @@ -53,6 +85,10 @@ func init() { SignatureVerificationTotal, SignatureVerificationDuration, SignatureVerificationErrors, + SigstoreVerificationTotal, + SigstoreVerificationDuration, + SigstoreTrustedRootAgeSeconds, + SLSAProvenanceVerifiedTotal, } { if err := metrics.Registry.Register(c); err != nil { if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { @@ -77,3 +113,19 @@ func RecordVerification(provider string, verified bool, auditMode bool) { func RecordError(provider string, errorType string) { SignatureVerificationErrors.WithLabelValues(provider, errorType).Inc() } + +func RecordSigstoreVerification(success bool, reason string) { + res := "failure" + if success { + res = "success" + } + SigstoreVerificationTotal.WithLabelValues(res, reason).Inc() +} + +func ObserveSigstoreTrustedRootAge(seconds float64) { + SigstoreTrustedRootAgeSeconds.Set(seconds) +} + +func RecordSLSAProvenance(result string) { + SLSAProvenanceVerifiedTotal.WithLabelValues(result).Inc() +} diff --git a/kagenti-operator/internal/signature/provider.go b/kagenti-operator/internal/signature/provider.go index b5c97f9d..3f6dcd65 100644 --- a/kagenti-operator/internal/signature/provider.go +++ b/kagenti-operator/internal/signature/provider.go @@ -77,3 +77,25 @@ func NewProvider(config *Config) (Provider, error) { return nil, fmt.Errorf("unknown provider type: %s (only 'x5c' is supported)", config.Type) } } + +// BundleVerificationResult is the outcome of Sigstore bundle verification on a +// SignedAgentCard (sigstore-a2a output). +type BundleVerificationResult struct { + Verified bool + Details string + // Absent is true when no attestations/signatureBundle was present (plain agent card). + // Per RFC, this is a graceful adoption path and must not hard-fail Ready. + Absent bool + + Identity string // Fulcio/OIDC identity (certificate SAN summary) + RekorLogIndex string + SLSARepository string + SLSACommitSHA string +} + +// BundleVerifier verifies embedded Sigstore bundles inside SignedAgentCard JSON. +type BundleVerifier interface { + VerifySignedAgentCard(ctx context.Context, signedAgentCardJSON []byte, + identityOverride *agentv1alpha1.SigstoreVerification) (*BundleVerificationResult, error) + Name() string +} diff --git a/kagenti-operator/internal/signature/sigstore.go b/kagenti-operator/internal/signature/sigstore.go new file mode 100644 index 00000000..333799d6 --- /dev/null +++ b/kagenti-operator/internal/signature/sigstore.go @@ -0,0 +1,302 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "time" + + "github.com/gowebpki/jcs" + agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" + sigbundle "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/sigstore/sigstore-go/pkg/verify" +) + +// SigstoreConfig configures Sigstore bundle verification (SignedAgentCard). +type SigstoreConfig struct { + // TrustedRootJSON when non-nil loads trust material via root.NewTrustedRootFromJSON (private/air-gapped). + TrustedRootJSON []byte + // UseStagingTUF uses Sigstore staging TUF mirror and root (for staging-signed cards). + UseStagingTUF bool + + OIDCIssuer string + CertificateIdentity string + CertificateIssuerReg string + CertificateIdentityReg string +} + +// SigstoreProvider implements BundleVerifier using sigstore-go. +type SigstoreProvider struct { + verifier *verify.Verifier + cfg SigstoreConfig + trustedLoadedAt time.Time +} + +// NewSigstoreProvider builds a verifier. Either TrustedRootJSON is set or the public good / staging TUF root is fetched. +func NewSigstoreProvider(cfg *SigstoreConfig) (*SigstoreProvider, error) { + if cfg == nil { + return nil, errors.New("sigstore config is nil") + } + if cfg.OIDCIssuer == "" || cfg.CertificateIdentity == "" { + return nil, errors.New("sigstore: certificate OIDC issuer and certificate identity are required") + } + + var tm root.TrustedMaterial + var err error + switch { + case len(cfg.TrustedRootJSON) > 0: + tr, err2 := root.NewTrustedRootFromJSON(cfg.TrustedRootJSON) + if err2 != nil { + return nil, fmt.Errorf("sigstore: parse trusted root: %w", err2) + } + tm = tr + default: + opts := tuf.DefaultOptions() + if cfg.UseStagingTUF { + opts.RepositoryBaseURL = tuf.StagingMirror + opts.Root = tuf.StagingRoot() + } + tm, err = root.NewLiveTrustedRoot(opts) + if err != nil { + return nil, fmt.Errorf("sigstore: load trusted root from TUF: %w", err) + } + } + + v, err := verify.NewVerifier(tm, + verify.WithTransparencyLog(1), + verify.WithObserverTimestamps(1), + verify.WithSignedCertificateTimestamps(1), + ) + if err != nil { + return nil, fmt.Errorf("sigstore: new verifier: %w", err) + } + + ObserveSigstoreTrustedRootAge(0) + return &SigstoreProvider{ + verifier: v, + cfg: *cfg, + trustedLoadedAt: time.Now(), + }, nil +} + +func (s *SigstoreProvider) Name() string { + return "sigstore" +} + +// TrustedRootAgeSeconds returns seconds since the provider was constructed (root loaded). +func (s *SigstoreProvider) TrustedRootAgeSeconds() float64 { + if s.trustedLoadedAt.IsZero() { + return 0 + } + return time.Since(s.trustedLoadedAt).Seconds() +} + +// VerifySignedAgentCard verifies attestations.signatureBundle from a SignedAgentCard JSON document. +func (s *SigstoreProvider) VerifySignedAgentCard(ctx context.Context, signedJSON []byte, + override *agentv1alpha1.SigstoreVerification, +) (*BundleVerificationResult, error) { + _ = ctx + ObserveSigstoreTrustedRootAge(s.TrustedRootAgeSeconds()) + + parsed, err := parseSignedAgentCardStructure(signedJSON) + if err != nil { + return nil, fmt.Errorf("sigstore: parse signed agent card: %w", err) + } + if parsed.Absent { + return &BundleVerificationResult{ + Verified: false, + Absent: true, + Details: "no attestations.signatureBundle on agent card document", + }, nil + } + + canonicalAgentCard, err := jcs.Transform(parsed.AgentCardRaw) + if err != nil { + return nil, fmt.Errorf("sigstore: canonicalize agentCard (RFC 8785): %w", err) + } + + sigEntity := sigbundle.Bundle{} + if err := sigEntity.UnmarshalJSON(parsed.BundleRaw); err != nil { + return nil, fmt.Errorf("sigstore: load signature bundle: %w", err) + } + + issuer := s.cfg.OIDCIssuer + san := s.cfg.CertificateIdentity + issuerReg := s.cfg.CertificateIssuerReg + sanReg := s.cfg.CertificateIdentityReg + if override != nil { + if override.CertificateOIDCIssuer != "" { + issuer = override.CertificateOIDCIssuer + } + if override.CertificateIdentity != "" { + san = override.CertificateIdentity + } + } + + certID, err := verify.NewShortCertificateIdentity(issuer, issuerReg, san, sanReg) + if err != nil { + return nil, fmt.Errorf("sigstore: certificate identity policy: %w", err) + } + + policy := verify.NewPolicy( + verify.WithArtifact(bytes.NewReader(canonicalAgentCard)), + verify.WithCertificateIdentity(certID), + ) + + start := time.Now() + res, err := s.verifier.Verify(&sigEntity, policy) + duration := time.Since(start).Seconds() + SigstoreVerificationDuration.WithLabelValues("sigstore").Observe(duration) + + if err != nil { + RecordSigstoreVerification(false, "verification_failed") + return &BundleVerificationResult{ + Verified: false, + Details: err.Error(), + }, nil + } + + out := &BundleVerificationResult{ + Verified: true, + Details: "Sigstore bundle verification succeeded", + } + if res.VerifiedIdentity != nil { + out.Identity = res.VerifiedIdentity.SubjectAlternativeName.SubjectAlternativeName + } + out.RekorLogIndex = rekorIndexFromBundle(&sigEntity) + + slsaRepo, slsaSha := parseProvenance(parsed.ProvenanceRaw) + out.SLSARepository = slsaRepo + out.SLSACommitSHA = slsaSha + + RecordSigstoreVerification(true, "verified") + if len(parsed.ProvenanceRaw) > 0 { + if slsaRepo != "" || slsaSha != "" { + RecordSLSAProvenance("ok") + } else { + RecordSLSAProvenance("unparsed") + } + } + + return out, nil +} + +type parsedSignedCard struct { + AgentCardRaw []byte + BundleRaw []byte + ProvenanceRaw []byte + Absent bool +} + +func parseSignedAgentCardStructure(signedJSON []byte) (*parsedSignedCard, error) { + var outer struct { + AgentCard json.RawMessage `json:"agentCard"` + Attestations json.RawMessage `json:"attestations"` + VerificationMaterial json.RawMessage `json:"verificationMaterial"` + } + if err := json.Unmarshal(signedJSON, &outer); err != nil { + return nil, err + } + + var att struct { + SignatureBundle json.RawMessage `json:"signatureBundle"` + ProvenanceBundle json.RawMessage `json:"provenanceBundle"` + } + var legacy struct { + SignatureBundle json.RawMessage `json:"signatureBundle"` + ProvenanceBundle json.RawMessage `json:"provenanceBundle"` + } + + var bundleRaw []byte + var provRaw []byte + + switch { + case len(outer.Attestations) > 0: + if err := json.Unmarshal(outer.Attestations, &att); err != nil { + return nil, err + } + bundleRaw = att.SignatureBundle + if len(att.ProvenanceBundle) > 0 { + provRaw = att.ProvenanceBundle + } + case len(outer.VerificationMaterial) > 0: + if err := json.Unmarshal(outer.VerificationMaterial, &legacy); err != nil { + return nil, err + } + bundleRaw = legacy.SignatureBundle + if len(legacy.ProvenanceBundle) > 0 { + provRaw = legacy.ProvenanceBundle + } + } + + if len(bundleRaw) == 0 || string(bundleRaw) == "null" { + return &parsedSignedCard{Absent: true}, nil + } + if len(outer.AgentCard) == 0 { + return nil, errors.New("signed agent card missing agentCard field") + } + + return &parsedSignedCard{ + AgentCardRaw: outer.AgentCard, + BundleRaw: bundleRaw, + ProvenanceRaw: provRaw, + }, nil +} + +func rekorIndexFromBundle(b *sigbundle.Bundle) string { + entries, err := b.TlogEntries() + if err != nil || len(entries) == 0 { + return "" + } + return strconv.FormatInt(entries[0].LogIndex(), 10) +} + +// parseProvenance extracts repository and commit from a minimal SLSA / in-toto provenance JSON blob. +func parseProvenance(raw []byte) (repository, revision string) { + if len(raw) == 0 { + return "", "" + } + var wrap struct { + Provenance json.RawMessage `json:"provenance"` + } + if err := json.Unmarshal(raw, &wrap); err == nil && len(wrap.Provenance) > 0 { + raw = wrap.Provenance + } + + var p struct { + BuildDefinition struct { + ExternalParameters struct { + Source struct { + Repository string `json:"repository"` + Revision string `json:"revision"` + } `json:"source"` + } `json:"externalParameters"` + } `json:"buildDefinition"` + } + if err := json.Unmarshal(raw, &p); err != nil { + return "", "" + } + return p.BuildDefinition.ExternalParameters.Source.Repository, + p.BuildDefinition.ExternalParameters.Source.Revision +} diff --git a/kagenti-operator/internal/signature/sigstore_parse_test.go b/kagenti-operator/internal/signature/sigstore_parse_test.go new file mode 100644 index 00000000..a715a950 --- /dev/null +++ b/kagenti-operator/internal/signature/sigstore_parse_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature + +import ( + "encoding/json" + "testing" +) + +func TestParseSignedAgentCardStructure_Absent(t *testing.T) { + plain := []byte(`{"name":"x","version":"1"}`) + got, err := parseSignedAgentCardStructure(plain) + if err != nil { + t.Fatal(err) + } + if !got.Absent { + t.Fatalf("expected absent for plain card, got %#v", got) + } +} + +func TestParseSignedAgentCardStructure_WithAttestations(t *testing.T) { + raw := []byte(`{ + "agentCard": {"name": "A", "version": "1"}, + "attestations": { + "signatureBundle": {"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "provenanceBundle": {"provenance": {"predicateType": "https://slsa.dev/provenance/v1"}} + } + }`) + got, err := parseSignedAgentCardStructure(raw) + if err != nil { + t.Fatal(err) + } + if got.Absent { + t.Fatal("expected bundle present") + } + var m map[string]interface{} + if err := json.Unmarshal(got.BundleRaw, &m); err != nil { + t.Fatal(err) + } + if m["mediaType"] == nil { + t.Fatalf("expected signature bundle mediaType, got %s", string(got.BundleRaw)) + } +} diff --git a/kagenti-operator/test/integration/identity_binding_integration_test.go b/kagenti-operator/test/integration/identity_binding_integration_test.go index c92997ff..f239bee5 100644 --- a/kagenti-operator/test/integration/identity_binding_integration_test.go +++ b/kagenti-operator/test/integration/identity_binding_integration_test.go @@ -20,6 +20,7 @@ import ( "time" agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" + "github.com/kagenti/operator/internal/agentcard" "github.com/kagenti/operator/internal/controller" "github.com/kagenti/operator/internal/signature" appsv1 "k8s.io/api/apps/v1" @@ -94,10 +95,12 @@ func cleanupNamespace(t *testing.T, ctx context.Context) { // but do NOT exercise the full fetch → sign → verify → bind end-to-end path. type mockFetcher struct{} -func (f *mockFetcher) Fetch(_ context.Context, _, _ string, _ string, _ string) (*agentv1alpha1.AgentCardData, error) { - return &agentv1alpha1.AgentCardData{ - Name: "test-agent", - URL: "http://test:8000", +func (f *mockFetcher) Fetch(_ context.Context, _, _ string, _ string, _ string) (*agentcard.FetchResult, error) { + return &agentcard.FetchResult{ + CardData: &agentv1alpha1.AgentCardData{ + Name: "test-agent", + URL: "http://test:8000", + }, }, nil } @@ -116,7 +119,7 @@ func (p *mockSignatureProvider) VerifySignature(_ context.Context, _ *agentv1alp }, nil } -func (p *mockSignatureProvider) Name() string { return "mock" } +func (p *mockSignatureProvider) Name() string { return "mock" } func (p *mockSignatureProvider) BundleHash() string { return "" } func TestIdentityBindingIntegration(t *testing.T) { diff --git a/kagenti-operator/test/integration/sigstore_verification_integration_test.go b/kagenti-operator/test/integration/sigstore_verification_integration_test.go new file mode 100644 index 00000000..e9e20e34 --- /dev/null +++ b/kagenti-operator/test/integration/sigstore_verification_integration_test.go @@ -0,0 +1,788 @@ +//go:build integration +// +build integration + +/* +Copyright 2026. + +Integration test for Sigstore SignedAgentCard Bundle Verification + +Run with: go test -v -tags=integration ./test/integration/... -timeout 5m -run TestSigstoreVerification +Prerequisites: kubectl configured with access to a Kubernetes cluster with kagenti CRDs installed +*/ + +package integration + +import ( + "context" + "fmt" + "testing" + "time" + + agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" + "github.com/kagenti/operator/internal/agentcard" + "github.com/kagenti/operator/internal/controller" + "github.com/kagenti/operator/internal/signature" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + sigstoreTestNamespace = "sigstore-test" + testOIDCIssuer = "https://token.actions.githubusercontent.com" + testIdentity = "https://github.com/kagenti/operator/.github/workflows/sign-agent-card.yml@refs/heads/main" + testRekorIndex = "123456789" + testSLSARepo = "https://github.com/kagenti/operator" + testSLSACommit = "abc123def456" +) + +// mockBundleVerifier simulates Sigstore bundle verification for integration tests +type mockBundleVerifier struct { + verified bool + absent bool + identity string + rekorLogIndex string + slsaRepository string + slsaCommitSHA string + shouldError bool + errorMsg string +} + +func (m *mockBundleVerifier) VerifySignedAgentCard(ctx context.Context, signedJSON []byte, override *agentv1alpha1.SigstoreVerification) (*signature.BundleVerificationResult, error) { + if m.shouldError { + return nil, fmt.Errorf("%s", m.errorMsg) + } + + return &signature.BundleVerificationResult{ + Verified: m.verified, + Absent: m.absent, + Details: "mock bundle verifier", + Identity: m.identity, + RekorLogIndex: m.rekorLogIndex, + SLSARepository: m.slsaRepository, + SLSACommitSHA: m.slsaCommitSHA, + }, nil +} + +func (m *mockBundleVerifier) Name() string { + return "mock-sigstore" +} + +// mockFetcherWithSignedCard returns a FetchResult with RawSignedAgentCardJSON +// to simulate fetching a SignedAgentCard from ConfigMap +type mockFetcherWithSignedCard struct { + cardData *agentv1alpha1.AgentCardData + rawSignedAgentCardJSON []byte +} + +func (f *mockFetcherWithSignedCard) Fetch(_ context.Context, _, _, _, _ string) (*agentcard.FetchResult, error) { + return &agentcard.FetchResult{ + CardData: f.cardData, + RawSignedAgentCardJSON: f.rawSignedAgentCardJSON, + }, nil +} + +func TestSigstoreVerificationIntegration(t *testing.T) { + ctx := context.Background() + + setupClient(t) + + // Create dedicated namespace for Sigstore tests + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: sigstoreTestNamespace, + }, + } + err := k8sClient.Create(ctx, ns) + if err != nil && !errors.IsAlreadyExists(err) { + t.Fatalf("Failed to create test namespace: %v", err) + } + defer func() { + _ = k8sClient.Delete(ctx, ns) + }() + + t.Run("Test1_ValidSignedAgentCard", testValidSignedAgentCard) + t.Run("Test2_AbsentBundle_PlainAgentCard", testAbsentBundlePlainAgentCard) + t.Run("Test3_InvalidBundle_AuditMode", testInvalidBundleAuditMode) + t.Run("Test4_InvalidBundle_EnforcementMode", testInvalidBundleEnforcementMode) + t.Run("Test5_SLSAProvenanceExtraction", testSLSAProvenanceExtraction) + t.Run("Test6_PerCardIdentityOverride", testPerCardIdentityOverride) +} + +func testValidSignedAgentCard(t *testing.T) { + ctx := context.Background() + deploymentName := "valid-signed-agent" + cardName := "valid-signed-card" + + t.Log("\n========================================") + t.Log("TEST 1: Valid SignedAgentCard Bundle Verification") + t.Log("========================================") + + // Create deployment and service + deployment := createSigstoreTestDeployment(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, deployment) + + service := createSigstoreTestService(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, service) + + waitForDeploymentAvailable(t, ctx, deploymentName, sigstoreTestNamespace, 30*time.Second) + + // Create AgentCard CR + agentCard := &agentv1alpha1.AgentCard{ + ObjectMeta: metav1.ObjectMeta{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + Spec: agentv1alpha1.AgentCardSpec{ + SyncPeriod: "30s", + TargetRef: &agentv1alpha1.TargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deploymentName, + }, + }, + } + + if err := k8sClient.Create(ctx, agentCard); err != nil { + t.Fatalf("Failed to create AgentCard: %v", err) + } + defer deleteSigstoreResource(ctx, agentCard) + + // Mock fetcher that returns SignedAgentCard JSON + signedCardJSON := []byte(`{"agentCard":{"name":"test"},"attestations":{"signatureBundle":{}}}`) + fetcher := &mockFetcherWithSignedCard{ + cardData: &agentv1alpha1.AgentCardData{ + Name: "Valid Signed Agent", + Version: "1.0.0", + URL: "http://test:8000", + }, + rawSignedAgentCardJSON: signedCardJSON, + } + + // Mock bundle verifier that returns success + bundleVerifier := &mockBundleVerifier{ + verified: true, + absent: false, + identity: testIdentity, + rekorLogIndex: testRekorIndex, + } + + // Create reconciler with Sigstore verification enabled + reconciler := &controller.AgentCardReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + AgentFetcher: fetcher, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: true, + SigstoreAuditMode: false, + } + + // First reconcile: add finalizer + _, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + }) + if err != nil { + t.Fatalf("First reconcile failed: %v", err) + } + + // Second reconcile: verify bundle + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + }) + if err != nil { + t.Fatalf("Second reconcile failed: %v", err) + } + + // Wait for status update + time.Sleep(100 * time.Millisecond) + + // Verify status fields + updatedCard := &agentv1alpha1.AgentCard{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}, updatedCard); err != nil { + t.Fatalf("Failed to get updated AgentCard: %v", err) + } + + // Assert Sigstore verification succeeded + if updatedCard.Status.SigstoreBundleVerified == nil || !*updatedCard.Status.SigstoreBundleVerified { + t.Errorf("Expected sigstoreBundleVerified=true, got: %v", updatedCard.Status.SigstoreBundleVerified) + } + + if updatedCard.Status.SigstoreIdentity != testIdentity { + t.Errorf("Expected sigstoreIdentity=%s, got: %s", testIdentity, updatedCard.Status.SigstoreIdentity) + } + + if updatedCard.Status.RekorLogIndex != testRekorIndex { + t.Errorf("Expected rekorLogIndex=%s, got: %s", testRekorIndex, updatedCard.Status.RekorLogIndex) + } + + // Verify SigstoreVerified condition + verified := false + for _, cond := range updatedCard.Status.Conditions { + if cond.Type == "SigstoreVerified" && cond.Status == metav1.ConditionTrue { + verified = true + break + } + } + if !verified { + t.Error("Expected SigstoreVerified condition to be True") + } + + t.Log("✓ Valid SignedAgentCard bundle verification succeeded") +} + +func testAbsentBundlePlainAgentCard(t *testing.T) { + ctx := context.Background() + deploymentName := "plain-agent" + cardName := "plain-card" + + t.Log("\n========================================") + t.Log("TEST 2: Absent Bundle (Plain AgentCard)") + t.Log("========================================") + + deployment := createSigstoreTestDeployment(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, deployment) + + service := createSigstoreTestService(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, service) + + waitForDeploymentAvailable(t, ctx, deploymentName, sigstoreTestNamespace, 30*time.Second) + + agentCard := &agentv1alpha1.AgentCard{ + ObjectMeta: metav1.ObjectMeta{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + Spec: agentv1alpha1.AgentCardSpec{ + TargetRef: &agentv1alpha1.TargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deploymentName, + }, + }, + } + + if err := k8sClient.Create(ctx, agentCard); err != nil { + t.Fatalf("Failed to create AgentCard: %v", err) + } + defer deleteSigstoreResource(ctx, agentCard) + + // Plain agent card (no SignedAgentCard wrapper) + fetcher := &mockFetcherWithSignedCard{ + cardData: &agentv1alpha1.AgentCardData{ + Name: "Plain Agent", + URL: "http://test:8000", + }, + rawSignedAgentCardJSON: nil, // No signed card JSON + } + + // Bundle verifier returns "absent" + bundleVerifier := &mockBundleVerifier{ + verified: false, + absent: true, + } + + reconciler := &controller.AgentCardReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + AgentFetcher: fetcher, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: true, + SigstoreAuditMode: false, + } + + // Reconcile + _, _ = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + if err != nil { + t.Fatalf("Reconcile failed: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + // Verify status + updatedCard := &agentv1alpha1.AgentCard{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}, updatedCard); err != nil { + t.Fatalf("Failed to get updated AgentCard: %v", err) + } + + // Should have sigstoreBundleVerified=false (absent) + if updatedCard.Status.SigstoreBundleVerified == nil || *updatedCard.Status.SigstoreBundleVerified { + t.Errorf("Expected sigstoreBundleVerified=false for absent bundle, got: %v", updatedCard.Status.SigstoreBundleVerified) + } + + // Verify condition shows bundle not found + foundAbsentCondition := false + for _, cond := range updatedCard.Status.Conditions { + if cond.Type == "SigstoreVerified" && cond.Reason == controller.ReasonSigstoreBundleMissing { + foundAbsentCondition = true + break + } + } + if !foundAbsentCondition { + t.Error("Expected SigstoreVerified condition with reason SigstoreBundleNotFound") + } + + t.Log("✓ Absent bundle handled gracefully (plain agent card)") +} + +func testInvalidBundleAuditMode(t *testing.T) { + ctx := context.Background() + deploymentName := "invalid-audit-agent" + cardName := "invalid-audit-card" + + t.Log("\n========================================") + t.Log("TEST 3: Invalid Bundle - Audit Mode") + t.Log("========================================") + + deployment := createSigstoreTestDeployment(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, deployment) + + service := createSigstoreTestService(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, service) + + waitForDeploymentAvailable(t, ctx, deploymentName, sigstoreTestNamespace, 30*time.Second) + + agentCard := &agentv1alpha1.AgentCard{ + ObjectMeta: metav1.ObjectMeta{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + Spec: agentv1alpha1.AgentCardSpec{ + TargetRef: &agentv1alpha1.TargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deploymentName, + }, + }, + } + + if err := k8sClient.Create(ctx, agentCard); err != nil { + t.Fatalf("Failed to create AgentCard: %v", err) + } + defer deleteSigstoreResource(ctx, agentCard) + + signedCardJSON := []byte(`{"agentCard":{"name":"test"},"attestations":{"signatureBundle":{}}}`) + fetcher := &mockFetcherWithSignedCard{ + cardData: &agentv1alpha1.AgentCardData{ + Name: "Invalid Bundle Agent", + URL: "http://test:8000", + }, + rawSignedAgentCardJSON: signedCardJSON, + } + + // Bundle verifier returns failure + bundleVerifier := &mockBundleVerifier{ + verified: false, + absent: false, + } + + reconciler := &controller.AgentCardReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + AgentFetcher: fetcher, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: true, + SigstoreAuditMode: true, // AUDIT MODE + } + + // Reconcile + _, _ = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + + // In audit mode, reconcile should NOT error even with invalid bundle + if err != nil { + t.Errorf("Audit mode should not error on invalid bundle, got: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + // Verify status + updatedCard := &agentv1alpha1.AgentCard{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}, updatedCard); err != nil { + t.Fatalf("Failed to get updated AgentCard: %v", err) + } + + // Should have sigstoreBundleVerified=false + if updatedCard.Status.SigstoreBundleVerified == nil || *updatedCard.Status.SigstoreBundleVerified { + t.Errorf("Expected sigstoreBundleVerified=false, got: %v", updatedCard.Status.SigstoreBundleVerified) + } + + // Verify condition shows audit mode failure + foundAuditCondition := false + for _, cond := range updatedCard.Status.Conditions { + if cond.Type == "SigstoreVerified" && cond.Reason == controller.ReasonSigstoreInvalidAudit { + foundAuditCondition = true + break + } + } + if !foundAuditCondition { + t.Error("Expected SigstoreVerified condition with reason SigstoreVerificationFailedAudit") + } + + t.Log("✓ Invalid bundle in audit mode logged but did not block reconciliation") +} + +func testInvalidBundleEnforcementMode(t *testing.T) { + ctx := context.Background() + deploymentName := "invalid-enforce-agent" + cardName := "invalid-enforce-card" + + t.Log("\n========================================") + t.Log("TEST 4: Invalid Bundle - Enforcement Mode") + t.Log("========================================") + + deployment := createSigstoreTestDeployment(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, deployment) + + service := createSigstoreTestService(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, service) + + waitForDeploymentAvailable(t, ctx, deploymentName, sigstoreTestNamespace, 30*time.Second) + + agentCard := &agentv1alpha1.AgentCard{ + ObjectMeta: metav1.ObjectMeta{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + Spec: agentv1alpha1.AgentCardSpec{ + TargetRef: &agentv1alpha1.TargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deploymentName, + }, + }, + } + + if err := k8sClient.Create(ctx, agentCard); err != nil { + t.Fatalf("Failed to create AgentCard: %v", err) + } + defer deleteSigstoreResource(ctx, agentCard) + + signedCardJSON := []byte(`{"agentCard":{"name":"test"},"attestations":{"signatureBundle":{}}}`) + fetcher := &mockFetcherWithSignedCard{ + cardData: &agentv1alpha1.AgentCardData{ + Name: "Invalid Bundle Agent", + URL: "http://test:8000", + }, + rawSignedAgentCardJSON: signedCardJSON, + } + + // Bundle verifier returns failure + bundleVerifier := &mockBundleVerifier{ + verified: false, + absent: false, + } + + reconciler := &controller.AgentCardReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + AgentFetcher: fetcher, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: true, + SigstoreAuditMode: false, // ENFORCEMENT MODE + } + + // Reconcile + _, _ = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + + // In enforcement mode, reconcile should succeed but card should be marked as invalid + if err != nil { + t.Errorf("Enforcement mode reconcile failed: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + // Verify status + updatedCard := &agentv1alpha1.AgentCard{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}, updatedCard); err != nil { + t.Fatalf("Failed to get updated AgentCard: %v", err) + } + + // Should have sigstoreBundleVerified=false + if updatedCard.Status.SigstoreBundleVerified == nil || *updatedCard.Status.SigstoreBundleVerified { + t.Errorf("Expected sigstoreBundleVerified=false in enforcement mode, got: %v", updatedCard.Status.SigstoreBundleVerified) + } + + // Verify Synced condition is False (card rejected) + syncedCondition := false + for _, cond := range updatedCard.Status.Conditions { + if cond.Type == "Synced" && cond.Status == metav1.ConditionFalse { + syncedCondition = true + break + } + } + if !syncedCondition { + t.Error("Expected Synced condition to be False in enforcement mode with invalid bundle") + } + + t.Log("✓ Invalid bundle in enforcement mode rejected agent card") +} + +func testSLSAProvenanceExtraction(t *testing.T) { + ctx := context.Background() + deploymentName := "slsa-agent" + cardName := "slsa-card" + + t.Log("\n========================================") + t.Log("TEST 5: SLSA Provenance Extraction") + t.Log("========================================") + + deployment := createSigstoreTestDeployment(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, deployment) + + service := createSigstoreTestService(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, service) + + waitForDeploymentAvailable(t, ctx, deploymentName, sigstoreTestNamespace, 30*time.Second) + + agentCard := &agentv1alpha1.AgentCard{ + ObjectMeta: metav1.ObjectMeta{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + Spec: agentv1alpha1.AgentCardSpec{ + TargetRef: &agentv1alpha1.TargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deploymentName, + }, + }, + } + + if err := k8sClient.Create(ctx, agentCard); err != nil { + t.Fatalf("Failed to create AgentCard: %v", err) + } + defer deleteSigstoreResource(ctx, agentCard) + + signedCardJSON := []byte(`{"agentCard":{"name":"test"},"attestations":{"signatureBundle":{},"provenanceBundle":{}}}`) + fetcher := &mockFetcherWithSignedCard{ + cardData: &agentv1alpha1.AgentCardData{ + Name: "SLSA Agent", + URL: "http://test:8000", + }, + rawSignedAgentCardJSON: signedCardJSON, + } + + // Bundle verifier returns success with SLSA provenance + bundleVerifier := &mockBundleVerifier{ + verified: true, + absent: false, + identity: testIdentity, + slsaRepository: testSLSARepo, + slsaCommitSHA: testSLSACommit, + } + + reconciler := &controller.AgentCardReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + AgentFetcher: fetcher, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: true, + SigstoreAuditMode: false, + } + + // Reconcile + _, _ = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + if err != nil { + t.Fatalf("Reconcile failed: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + // Verify SLSA provenance fields + updatedCard := &agentv1alpha1.AgentCard{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}, updatedCard); err != nil { + t.Fatalf("Failed to get updated AgentCard: %v", err) + } + + if updatedCard.Status.SLSARepository != testSLSARepo { + t.Errorf("Expected slsaRepository=%s, got: %s", testSLSARepo, updatedCard.Status.SLSARepository) + } + + if updatedCard.Status.SLSACommitSHA != testSLSACommit { + t.Errorf("Expected slsaCommitSHA=%s, got: %s", testSLSACommit, updatedCard.Status.SLSACommitSHA) + } + + t.Log("✓ SLSA provenance extracted successfully") +} + +func testPerCardIdentityOverride(t *testing.T) { + ctx := context.Background() + deploymentName := "override-agent" + cardName := "override-card" + + t.Log("\n========================================") + t.Log("TEST 6: Per-Card Identity Override") + t.Log("========================================") + + deployment := createSigstoreTestDeployment(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, deployment) + + service := createSigstoreTestService(t, ctx, sigstoreTestNamespace, deploymentName) + defer deleteSigstoreResource(ctx, service) + + waitForDeploymentAvailable(t, ctx, deploymentName, sigstoreTestNamespace, 30*time.Second) + + // AgentCard with custom Sigstore verification settings + customIdentity := "https://custom.identity.example.com" + customIssuer := "https://custom.issuer.example.com" + + agentCard := &agentv1alpha1.AgentCard{ + ObjectMeta: metav1.ObjectMeta{ + Name: cardName, + Namespace: sigstoreTestNamespace, + }, + Spec: agentv1alpha1.AgentCardSpec{ + TargetRef: &agentv1alpha1.TargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deploymentName, + }, + SigstoreVerification: &agentv1alpha1.SigstoreVerification{ + CertificateIdentity: customIdentity, + CertificateOIDCIssuer: customIssuer, + }, + }, + } + + if err := k8sClient.Create(ctx, agentCard); err != nil { + t.Fatalf("Failed to create AgentCard: %v", err) + } + defer deleteSigstoreResource(ctx, agentCard) + + signedCardJSON := []byte(`{"agentCard":{"name":"test"},"attestations":{"signatureBundle":{}}}`) + fetcher := &mockFetcherWithSignedCard{ + cardData: &agentv1alpha1.AgentCardData{ + Name: "Override Agent", + URL: "http://test:8000", + }, + rawSignedAgentCardJSON: signedCardJSON, + } + + bundleVerifier := &mockBundleVerifier{ + verified: true, + absent: false, + identity: customIdentity, // Should match the override + } + + reconciler := &controller.AgentCardReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + AgentFetcher: fetcher, + BundleVerifier: bundleVerifier, + EnableSigstoreVerification: true, + SigstoreAuditMode: false, + } + + // Reconcile + _, _ = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}}) + if err != nil { + t.Fatalf("Reconcile failed: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + // Verify custom identity was used + updatedCard := &agentv1alpha1.AgentCard{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cardName, Namespace: sigstoreTestNamespace}, updatedCard); err != nil { + t.Fatalf("Failed to get updated AgentCard: %v", err) + } + + if updatedCard.Status.SigstoreIdentity != customIdentity { + t.Errorf("Expected custom identity=%s, got: %s", customIdentity, updatedCard.Status.SigstoreIdentity) + } + + t.Log("✓ Per-card identity override worked correctly") +} + +// Helper functions for Sigstore integration tests + +func createSigstoreTestDeployment(t *testing.T, ctx context.Context, namespace, name string) *appsv1.Deployment { + t.Helper() + labels := map[string]string{ + "app.kubernetes.io/name": name, + "kagenti.io/type": "agent", + "protocol.kagenti.io/a2a": "", + } + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": name}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "agent", + Image: "registry.k8s.io/pause:3.9", + }, + }, + }, + }, + }, + } + + if err := k8sClient.Create(ctx, deployment); err != nil { + t.Fatalf("Failed to create deployment: %v", err) + } + + t.Logf(" Created Deployment: %s", name) + return deployment +} + +func createSigstoreTestService(t *testing.T, ctx context.Context, namespace, name string) *corev1.Service { + t.Helper() + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{"app": name}, + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 8000, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + } + + if err := k8sClient.Create(ctx, service); err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + t.Logf(" Created Service: %s", name) + return service +} + +func deleteSigstoreResource(ctx context.Context, obj client.Object) { + // Remove finalizers first (controller adds finalizer to AgentCards) + obj.SetFinalizers(nil) + _ = k8sClient.Update(ctx, obj) + _ = k8sClient.Delete(ctx, obj) +} diff --git a/kagenti-operator/test/integration/trust_bundle_rotation_integration_test.go b/kagenti-operator/test/integration/trust_bundle_rotation_integration_test.go index a14308b1..5e804e74 100644 --- a/kagenti-operator/test/integration/trust_bundle_rotation_integration_test.go +++ b/kagenti-operator/test/integration/trust_bundle_rotation_integration_test.go @@ -103,11 +103,11 @@ func TestTrustBundleRotation(t *testing.T) { } reconciler := &controller.AgentCardReconciler{ - Client: k8sClient, - Scheme: scheme, - AgentFetcher: &mockFetcher{}, - SignatureProvider: provider, - RequireSignature: true, + Client: k8sClient, + Scheme: scheme, + AgentFetcher: &mockFetcher{}, + SignatureProvider: provider, + RequireSignature: true, SVIDExpiryGracePeriod: 1 * time.Millisecond, }