Repository: deislabs/ratify
Pull request #519 (2022-12-21 19:06:32)

feat: plugins as OCI artifacts
# Description

## What this PR does / why we need it:

Adds the ability for Ratify to download plugins from OCI artifacts as they are registered, per https://github.com/deislabs/ratify/issues/499

2023-01-10 community call demo: https://youtu.be/xRo8U-ag0y0?t=3047

This eliminates the need for users to build their own Ratify image, hack the Helm chart output, etc. It's an opt-in change that doesn't break how Ratify works today, but only enhances the experience if the `RATIFY_DYNAMIC_PLUGINS=1` feature flag is enabled, and `source` is specified in the config

Here's a high level overview of the experience:

### 1. Making a plugin available

We start by uploading a plugin to a registry as an OCI artifact.

This could be done by 
* an org's platform team
  * build it yourself
  * download from a url and upload into a trusted registry
* direct pull from a vendor's trusted registry
  * ex: I create a useful plugin, push it to `noelregistry.azurecr.io/usefulplugin:v1`, and you refer to that directly
  * this is **possible** but seems less likely?

Here's an example using the `sample` verifier in the repo

```shell
cd plugins/verifier/sample
CGO_ENABLED=0 go build .
oras push noelfdpo.azurecr.io/sample-plugin:v1 ./sample
```

If there are "off the shelf" plugins already built by community partners, this might look something like

```shell
curl -L https://github.com/deislabs/ratify/releases/download/v1.0.0-beta.2/ratify_verifier_sbom_plugin -o ./sbom
oras push noelfdpo.azurecr.io/sbom:v1.0.0-beta.2 ./sbom
```

### 2. Using a plugin

A new `source` property has been added for verifier and referrerstore config. If specified, Ratify will
* resolve/download the artifact
* mark it executable
* continue with registration & execution of the binary plugin as it does today

The plugin download process uses the same authProvider(s) that the `oras` built-in store uses

This works with both flavors of config - JSON and CRD. Below are examples of both

#### config.json

* using an unspecified `authProvider` - defaults to `dockerConfig` like the ORAS plugin
* pulling from Docker Hub

```json
{
  "name": "sample",
  "artifactTypes": "application/spdx+json",
  "source": {
    "artifact": "index.docker.io/acanthamoeba/sample-plugin:v1"
  }
}
```

#### CRD

* pulling from Azure Container Registry
* specifying workload identity as the auth provider to talk to the registry

```yaml
apiVersion: config.ratify.deislabs.io/v1alpha1
kind: Verifier
metadata:
  name: verifier-sample
spec:
  name: sample
  artifactTypes: application/spdx+json
  source:
    artifact: noelfdpo.azurecr.io/sample-plugin:v1
    authProvider:
      name: azureWorkloadIdentity
```

### Other info

Ratify emits info/debug logs when downloading plugins, ex:

```text
time="2022-12-21T18:39:11Z" level=info msg="Downloading plugin sample from noelfdpo.azurecr.io/sample-plugin:v1"
time="2022-12-21T18:39:11Z" level=info msg="selected default auth provider: dockerConfig"
time="2022-12-21T18:39:12Z" level=debug msg="Resolved plugin manifest: {application/vnd.oci.artifact.manifest.v1+json sha256:f2c54ba629c4da68327efa4085e7e322964c6533f7c9bcfd8d69202950e20d7f 413 [] map[] [] <nil> }"
time="2022-12-21T18:39:12Z" level=debug msg="Downloading blob noelfdpo.azurecr.io/sample-plugin:v1@sha256:814191e0d58143ebbcad493d437d1d3cabdac4a0706cb6dff8efa98afd2e31b4"
time="2022-12-21T18:39:12Z" level=info msg="Downloading plugin sample to /.ratify/plugins/sample"
time="2022-12-21T18:39:13Z" level=debug msg="Marking plugin /.ratify/plugins/sample as executable"
time="2022-12-21T18:39:13Z" level=info msg="downloaded verifier plugin sample from noelfdpo.azurecr.io/sample-plugin:v1 to /.ratify/plugins"
time="2022-12-21T18:39:13Z" level=info msg="verifier 'sample' added to verifier map"
```

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Please also list any relevant details for your test configuration

- [x] e2e cli tests
- [x] e2e tests
  - **note: :`helm upgrade` to set `featureFlags.RATIFY_DYNAMIC_PLUGINS=true` fails in CI, so one of the test cases is currently skipped**

# Checklist:

- [x] Does the affected code have corresponding tests?
- [x] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
  - No, this is an opt-in feature behind a disabled-by-default feature flag
- [x] Do all new files have appropriate license header?


noelbundick-msft: @susanshi This is now updated to use the the `wabbitnetworks.azurecr.io` sample plugins, merged with latest, with all tests passing

Please let me know if there's anything else on my side. Thanks!
susanshi: > @susanshi This is now updated to use the the `wabbitnetworks.azurecr.io` sample plugins, merged with latest, with all tests passing
> 
> Please let me know if there's anything else on my side. Thanks!

 we have finished merging all RC required PRs,  this PR is good to go with a rebase,

Please rebase , we will hold off any changes into the main branch to avoid this being out of date again. thanks @noelbundick-msft .

Patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index aa55e66..7717cf9 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -31,6 +31,10 @@ RUN curl -Lo oras.tar.gz https://github.com/oras-project/oras/releases/download/
   && mv oras-install/oras /usr/local/bin/ \
   && rm -rf ./oras-install oras.tar.gz
 
+ARG KUBEBUILDER_VERSION="3.8.0"
+RUN curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_linux_amd64 -o /usr/local/bin/kubebuilder \
+  && chmod +x /usr/local/bin/kubebuilder
+
 # [Optional] Uncomment this section to install additional OS packages.
 RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
   && apt-get -y install --no-install-recommends protobuf-compiler libprotobuf-dev
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 421fe8c..3a0d8c1 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,6 +10,9 @@
       "request": "launch",
       "mode": "auto",
       "program": "${workspaceFolder}/cmd/ratify",
+      "env": {
+        "RATIFY_DYNAMIC_PLUGINS": "1"
+      },
       "args": [
         "verify",
         "-s",
@@ -22,6 +25,9 @@
       "request": "launch",
       "mode": "auto",
       "program": "${workspaceFolder}/cmd/ratify",
+      "env": {
+        "RATIFY_DYNAMIC_PLUGINS": "1"
+      },
       "args": [
         "serve",
         "--http",
@@ -36,7 +42,8 @@
       "mode": "auto",
       "program": "${workspaceFolder}/cmd/ratify",
       "env": {
-        "RATIFY_LOG_LEVEL": "debug"
+        "RATIFY_LOG_LEVEL": "debug",
+        "RATIFY_DYNAMIC_PLUGINS": "1"
       },
       "args": [
         "serve",
diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go
new file mode 100644
index 0000000..0808c56
--- /dev/null
+++ b/api/v1alpha1/common.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2022.
+
+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 v1alpha1
+
+import runtime "k8s.io/apimachinery/pkg/runtime"
+
+// PluginSource defines the fields needed to download a plugin from an OCI Artifact source
+type PluginSource struct {
+	// Important: Run "make" to regenerate code after modifying this file
+
+	// OCI Artifact source to download the plugin from
+	Artifact string `json:"artifact,omitempty"`
+
+	// +kubebuilder:pruning:PreserveUnknownFields
+	// AuthProvider to use to authenticate to the OCI Artifact source, optional
+	AuthProvider runtime.RawExtension `json:"authProvider,omitempty"`
+}
diff --git a/api/v1alpha1/store_types.go b/api/v1alpha1/store_types.go
index 065bd0f..4d37950 100644
--- a/api/v1alpha1/store_types.go
+++ b/api/v1alpha1/store_types.go
@@ -29,6 +29,8 @@ type StoreSpec struct {
 	Name string `json:"name,omitempty"`
 	// Plugin path, optional
 	Address string `json:"address,omitempty"`
+	// OCI Artifact source to download the plugin from, optional
+	Source *PluginSource `json:"source,omitempty"`
 
 	// +kubebuilder:pruning:PreserveUnknownFields
 	// Parameters of the store
diff --git a/api/v1alpha1/verifier_types.go b/api/v1alpha1/verifier_types.go
index ec11565..07fc456 100644
--- a/api/v1alpha1/verifier_types.go
+++ b/api/v1alpha1/verifier_types.go
@@ -34,6 +34,9 @@ type VerifierSpec struct {
 	// # Optional. URL/file path
 	Address string `json:"address,omitempty"`
 
+	// OCI Artifact source to download the plugin from, optional
+	Source *PluginSource `json:"source,omitempty"`
+
 	// +kubebuilder:pruning:PreserveUnknownFields
 	// Parameters for this verifier
 	Parameters runtime.RawExtension `json:"parameters,omitempty"`
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index d99c32a..e51352f 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -115,6 +115,22 @@ func (in *CertificateStoreStatus) DeepCopy() *CertificateStoreStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PluginSource) DeepCopyInto(out *PluginSource) {
+	*out = *in
+	in.AuthProvider.DeepCopyInto(&out.AuthProvider)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginSource.
+func (in *PluginSource) DeepCopy() *PluginSource {
+	if in == nil {
+		return nil
+	}
+	out := new(PluginSource)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Store) DeepCopyInto(out *Store) {
 	*out = *in
@@ -177,6 +193,11 @@ func (in *StoreList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StoreSpec) DeepCopyInto(out *StoreSpec) {
 	*out = *in
+	if in.Source != nil {
+		in, out := &in.Source, &out.Source
+		*out = new(PluginSource)
+		(*in).DeepCopyInto(*out)
+	}
 	in.Parameters.DeepCopyInto(&out.Parameters)
 }
 
@@ -267,6 +288,11 @@ func (in *VerifierList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VerifierSpec) DeepCopyInto(out *VerifierSpec) {
 	*out = *in
+	if in.Source != nil {
+		in, out := &in.Source, &out.Source
+		*out = new(PluginSource)
+		(*in).DeepCopyInto(*out)
+	}
 	in.Parameters.DeepCopyInto(&out.Parameters)
 }
 
diff --git a/charts/ratify/crds/store-customresourcedefinition.yaml b/charts/ratify/crds/store-customresourcedefinition.yaml
index 0d49145..b46587f 100644
--- a/charts/ratify/crds/store-customresourcedefinition.yaml
+++ b/charts/ratify/crds/store-customresourcedefinition.yaml
@@ -39,10 +39,22 @@ spec:
                 description: Parameters of this store
                 type: object
                 x-kubernetes-preserve-unknown-fields: true
+              source:
+                description: OCI Artifact source to download the plugin from, optional
+                properties:
+                  artifact:
+                    description: OCI Artifact source to download the plugin from
+                    type: string
+                  authProvider:
+                    description: AuthProvider to use to authenticate to the OCI Artifact
+                      source, optional
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
             type: object
           status:
             description: StoreStatus defines the observed state of Store
             type: object
         type: object
     served: true
-    storage: true
\ No newline at end of file
+    storage: true
diff --git a/charts/ratify/crds/verifier-customresourcedefinition.yaml b/charts/ratify/crds/verifier-customresourcedefinition.yaml
index a436879..a4ba279 100644
--- a/charts/ratify/crds/verifier-customresourcedefinition.yaml
+++ b/charts/ratify/crds/verifier-customresourcedefinition.yaml
@@ -42,10 +42,22 @@ spec:
                 description: Parameters of this verifier
                 type: object
                 x-kubernetes-preserve-unknown-fields: true
+              source:
+                description: OCI Artifact source to download the plugin from, optional
+                properties:
+                  artifact:
+                    description: OCI Artifact source to download the plugin from
+                    type: string
+                  authProvider:
+                    description: AuthProvider to use to authenticate to the OCI Artifact
+                      source, optional
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
             type: object
           status:
             description: VerifierStatus defines the observed state of Verifier
             type: object
         type: object
     served: true
-    storage: true
\ No newline at end of file
+    storage: true
diff --git a/config/crd/bases/config.ratify.deislabs.io_stores.yaml b/config/crd/bases/config.ratify.deislabs.io_stores.yaml
index f0476b0..531a362 100644
--- a/config/crd/bases/config.ratify.deislabs.io_stores.yaml
+++ b/config/crd/bases/config.ratify.deislabs.io_stores.yaml
@@ -45,6 +45,18 @@ spec:
                 description: Parameters of the store
                 type: object
                 x-kubernetes-preserve-unknown-fields: true
+              source:
+                description: OCI Artifact source to download the plugin from, optional
+                properties:
+                  artifact:
+                    description: OCI Artifact source to download the plugin from
+                    type: string
+                  authProvider:
+                    description: AuthProvider to use to authenticate to the OCI Artifact
+                      source, optional
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
             type: object
           status:
             description: StoreStatus defines the observed state of Store
diff --git a/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml b/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml
index 8c30590..6ed3ffc 100644
--- a/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml
+++ b/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml
@@ -48,6 +48,18 @@ spec:
                 description: Parameters for this verifier
                 type: object
                 x-kubernetes-preserve-unknown-fields: true
+              source:
+                description: OCI Artifact source to download the plugin from, optional
+                properties:
+                  artifact:
+                    description: OCI Artifact source to download the plugin from
+                    type: string
+                  authProvider:
+                    description: AuthProvider to use to authenticate to the OCI Artifact
+                      source, optional
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
             type: object
           status:
             description: VerifierStatus defines the observed state of Verifier
diff --git a/config/samples/config_v1alpha1_verifier_dynamic.yaml b/config/samples/config_v1alpha1_verifier_dynamic.yaml
new file mode 100644
index 0000000..06d54cc
--- /dev/null
+++ b/config/samples/config_v1alpha1_verifier_dynamic.yaml
@@ -0,0 +1,9 @@
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: Verifier
+metadata:
+  name: verifier-dynamic
+spec:
+  name: dynamic
+  artifactTypes: application/vnd.ratify.spdx.v0
+  source:
+    artifact: wabbitnetworks.azurecr.io/test/sample-verifier-plugin:v1
diff --git a/docs/reference/dynamic-plugins.md b/docs/reference/dynamic-plugins.md
new file mode 100644
index 0000000..5d9b3b2
--- /dev/null
+++ b/docs/reference/dynamic-plugins.md
@@ -0,0 +1,78 @@
+# Dynamic Plugins
+
+While Ratify provides a number of built-in referrer stores and verifiers, there may be times that you need to add additional out-of-tree plugins to verify additional artifact types or check for specific conditions. Whether you've followed the [plugin authoring guide](./creating-plugins.md), or you want to use a plugin that's provided by a third party, the next step is to make your plugin available to be called at runtime.
+
+Ratify includes an optional dynamic plugins feature to simplify plugin distribution. This removes the need to rebuild or maintain your own Ratify container image. When enabled, plugins are stored as OCI artifacts and pulled down at runtime when they are registered via CRD. This guide will show you how to activate and configure dynamic plugins.
+
+## Uploading your plugin
+
+Upload your plugin to a registry that supports OCI artifacts. Here we're using the sample plugin located at `plugins/verifier/sample`. Replace the registry values with your own.
+
+```shell
+CGO_ENABLED=0 go build -o=./sample ./plugins/verifier/sample
+oras push myregistry.azurecr.io/sample-plugin:v1 ./sample
+```
+
+> It's important to build plugins with `CGO_ENABLED=0` so that they can run inside the Ratify [distroless](https://github.com/GoogleContainerTools/distroless) container
+
+## Ratify Configuration
+
+Now that you have a plugin available as an OCI artifact, it's time to enable the `RATIFY_DYNAMIC_PLUGINS` [feature flag](/docs/reference/usage.md#feature-flags). Here, we enable it via Helm chart parameter:
+
+```shell
+# Option 1: to enable on a fresh install
+helm install ratify \
+    ratify/ratify --atomic \
+    --set-file provider.tls.crt=${CERT_DIR}/server.crt \
+    --set-file provider.tls.key=${CERT_DIR}/server.key \
+    --set provider.tls.cabundle="$(cat ${CERT_DIR}/ca.crt | base64 | tr -d '\n')" \
+    --set featureFlags.RATIFY_DYNAMIC_PLUGINS=true
+
+# Option 2: to enable on a previously-installed release
+helm upgrade ratify \
+  ratify/ratify --atomic \
+  --reuse-values \
+  --set featureFlags.RATIFY_DYNAMIC_PLUGINS=true
+```
+
+## Plugin Configuration
+
+The last step is to specify the optional `source` property to tell Ratify where to download the plugin from. Dynamic plugins use the same [auth providers](/docs/reference/oras-auth-provider.md) and options as the builtin ORAS store (ex: `azureWorkloadIdentity`, `awsEcrBasic`, `k8Secrets`) for authentication to the registry where your plugins are located.
+
+```yaml
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: Verifier
+metadata:
+  name: verifier-sample
+spec:
+  name: sample
+  artifactTypes: application/vnd.ratify.spdx.v0
+  source:
+    artifact: myregistry.azurecr.io/sample-plugin:v1
+    authProvider:
+      name: azureWorkloadIdentity
+```
+
+## Confirmation / Troubleshooting
+
+You can check the Ratify logs for more details on which plugin(s) were downloaded. Your specific commands may vary slightly based on the values you provided during chart installation.
+
+```shell
+kubectl logs -n gatekeeper-system deployment/ratify
+```
+
+This will generate output similar to below, which can be used for confirmation of a successful plugin download or to aid in troubleshooting.
+
+```text
+time="2023-01-18T16:44:46Z" level=info msg="Setting log level to info"
+time="2023-01-18T16:44:46Z" level=info msg="Feature flag DYNAMIC_PLUGINS is enabled"
+time="2023-01-18T16:44:46Z" level=info msg="starting crd manager"
+time="2023-01-18T16:44:46Z" level=info msg="initializing executor with config file at default config path"
+<snip>
+time="2023-01-18T16:44:46Z" level=info msg="reconciling verifier 'verifier-dynamic'"
+time="2023-01-18T16:44:46Z" level=info msg="Address was empty, setting to default path: /.ratify/plugins"
+time="2023-01-18T16:44:47Z" level=info msg="selected auth provider: azureWorkloadIdentity"
+time="2023-01-18T16:44:48Z" level=info msg="downloaded verifier plugin dynamic from myregistry.azurecr.io/sample-plugin:v1 to /.ratify/plugins/dynamic"
+time="2023-01-18T16:44:48Z" level=info msg="verifier 'dynamic' added to verifier map"
+<snip>
+```
diff --git a/docs/reference/usage.md b/docs/reference/usage.md
index d057b64..d8ad5ce 100644
--- a/docs/reference/usage.md
+++ b/docs/reference/usage.md
@@ -18,3 +18,5 @@ This page documents useful flags and options supported by Ratify
 
 Ratify may roll out new features behind feature flags, which are activated by setting the corresponding environment variable `RATIFY_<FEATURE_NAME>=1`.
 A value of `1` indicates the feature is active; any other value disables the flag.
+
+- `RATIFY_DYNAMIC_PLUGINS`: (disabled) Enables Ratify to download plugins at runtime from an OCI registry by setting `source` on the plugin config
diff --git a/httpserver/Dockerfile b/httpserver/Dockerfile
index 55c51e7..ac181f1 100644
--- a/httpserver/Dockerfile
+++ b/httpserver/Dockerfile
@@ -35,7 +35,7 @@ ARG RATIFY_FOLDER=$HOME/.ratify/
 WORKDIR /
 
 COPY --from=builder /app/out/ratify /app/
-COPY --from=builder /app/out/plugins ${RATIFY_FOLDER}/plugins
+COPY --from=builder --chown=65532:65532 /app/out/plugins ${RATIFY_FOLDER}/plugins
 COPY --from=builder /app/config/config.json ${RATIFY_FOLDER}
 
 ENV RATIFY_CONFIG=${RATIFY_FOLDER}
diff --git a/pkg/common/plugin/download.go b/pkg/common/plugin/download.go
new file mode 100644
index 0000000..3b60bd6
--- /dev/null
+++ b/pkg/common/plugin/download.go
@@ -0,0 +1,143 @@
+/*
+Copyright The Ratify Authors.
+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 plugin
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/deislabs/ratify/pkg/common/oras/authprovider"
+	"github.com/deislabs/ratify/pkg/ocispecs"
+	"github.com/sirupsen/logrus"
+	"oras.land/oras-go/v2/registry/remote"
+	"oras.land/oras-go/v2/registry/remote/auth"
+)
+
+type PluginSource struct {
+	Artifact     string                          `json:"artifact"`
+	AuthProvider authprovider.AuthProviderConfig `json:"authProvider,omitempty"`
+}
+
+func ParsePluginSource(source interface{}) (PluginSource, error) {
+	var pluginSource PluginSource
+
+	// parse to/from json
+	sourceBytes, err := json.Marshal(source)
+	if err != nil {
+		return pluginSource, err
+	}
+
+	err = json.Unmarshal(sourceBytes, &pluginSource)
+	if err != nil {
+		return pluginSource, err
+	}
+
+	return pluginSource, nil
+}
+
+func DownloadPlugin(source PluginSource, targetPath string) error {
+	ctx := context.TODO()
+
+	// initialize a repository
+	repository, err := remote.NewRepository(source.Artifact)
+	if err != nil {
+		return err
+	}
+
+	repository.Client = &auth.Client{
+		Client: &http.Client{Timeout: 10 * time.Second, Transport: http.DefaultTransport.(*http.Transport).Clone()},
+		Header: http.Header{
+			"User-Agent": {"ratify"},
+		},
+		Cache: auth.NewCache(),
+		Credential: func(ctx context.Context, registry string) (auth.Credential, error) {
+			authProvider, err := authprovider.CreateAuthProviderFromConfig(source.AuthProvider)
+			if err != nil {
+				return auth.EmptyCredential, err
+			}
+
+			authConfig, err := authProvider.Provide(ctx, registry)
+			if err != nil {
+				return auth.EmptyCredential, err
+			}
+
+			if authConfig.Username != "" || authConfig.Password != "" || authConfig.IdentityToken != "" {
+				return auth.Credential{
+					Username:     authConfig.Username,
+					Password:     authConfig.Password,
+					RefreshToken: authConfig.IdentityToken,
+				}, nil
+			}
+			return auth.EmptyCredential, nil
+		},
+	}
+
+	// read the reference manifest
+	referenceManifestDescriptor, err := repository.Resolve(ctx, source.Artifact)
+	if err != nil {
+		return err
+	}
+	logrus.Debugf("Resolved plugin manifest: %v", referenceManifestDescriptor)
+
+	manifestReader, err := repository.Fetch(ctx, referenceManifestDescriptor)
+	if err != nil {
+		return err
+	}
+
+	manifestBytes, err := io.ReadAll(manifestReader)
+	if err != nil {
+		return err
+	}
+
+	referenceManifest := ocispecs.ReferenceManifest{}
+	if err := json.Unmarshal(manifestBytes, &referenceManifest); err != nil {
+		return err
+	}
+
+	// download the first blob to the target path
+	blobReference := fmt.Sprintf("%s@%s", source, referenceManifest.Blobs[0].Digest)
+	logrus.Debugf("Downloading blob %s", blobReference)
+	_, blobReader, err := repository.Blobs().FetchReference(ctx, blobReference)
+	if err != nil {
+		return err
+	}
+
+	logrus.Debugf("writing plugin bytes to %s", targetPath)
+	pluginFile, err := os.Create(targetPath)
+	if err != nil {
+		return err
+	}
+
+	defer pluginFile.Close()
+	_, err = io.Copy(pluginFile, blobReader)
+	if err != nil {
+		return err
+	}
+
+	// mark the plugin as executable
+	logrus.Debugf("marking %s as executable", targetPath)
+	err = os.Chmod(targetPath, 0700)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/controllers/store_controller.go b/pkg/controllers/store_controller.go
index fe5949e..4f6dcf6 100644
--- a/pkg/controllers/store_controller.go
+++ b/pkg/controllers/store_controller.go
@@ -130,6 +130,9 @@ func specToStoreConfig(storeSpec configv1alpha1.StoreSpec) (rc.StorePluginConfig
 		}
 	}
 	storeConfig[types.Name] = storeSpec.Name
+	if storeSpec.Source != nil {
+		storeConfig[types.Source] = storeSpec.Source
+	}
 
 	return storeConfig, nil
 }
diff --git a/pkg/controllers/verifier_controller.go b/pkg/controllers/verifier_controller.go
index 7b12ed4..9e9149a 100644
--- a/pkg/controllers/verifier_controller.go
+++ b/pkg/controllers/verifier_controller.go
@@ -130,6 +130,9 @@ func specToVerifierConfig(verifierSpec configv1alpha1.VerifierSpec) (vc.Verifier
 
 	verifierConfig[types.Name] = verifierSpec.Name
 	verifierConfig[types.ArtifactTypes] = verifierSpec.ArtifactTypes
+	if verifierSpec.Source != nil {
+		verifierConfig[types.Source] = verifierSpec.Source
+	}
 
 	return verifierConfig, nil
 }
diff --git a/pkg/referrerstore/factory/factory.go b/pkg/referrerstore/factory/factory.go
index 25b111f..9524eb1 100644
--- a/pkg/referrerstore/factory/factory.go
+++ b/pkg/referrerstore/factory/factory.go
@@ -19,12 +19,16 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"path"
 	"strings"
 
+	pluginCommon "github.com/deislabs/ratify/pkg/common/plugin"
+	"github.com/deislabs/ratify/pkg/featureflag"
 	"github.com/deislabs/ratify/pkg/referrerstore"
 	"github.com/deislabs/ratify/pkg/referrerstore/config"
 	"github.com/deislabs/ratify/pkg/referrerstore/plugin"
 	"github.com/deislabs/ratify/pkg/referrerstore/types"
+	"github.com/sirupsen/logrus"
 )
 
 var builtInStores = make(map[string]StoreFactory)
@@ -57,6 +61,25 @@ func CreateStoreFromConfig(storeConfig config.StorePluginConfig, configVersion s
 		return nil, fmt.Errorf("invalid plugin name for a store: %s", storeName)
 	}
 
+	// if source is specified, download the plugin
+	if source, ok := storeConfig[types.Source]; ok {
+		if featureflag.DynamicPlugins.Enabled {
+			source, err := pluginCommon.ParsePluginSource(source)
+			if err != nil {
+				return nil, fmt.Errorf("failed to parse plugin source: %w", err)
+			}
+
+			targetPath := path.Join(pluginBinDir[0], storeNameStr)
+			err = pluginCommon.DownloadPlugin(source, targetPath)
+			if err != nil {
+				return nil, fmt.Errorf("failed to download plugin: %w", err)
+			}
+			logrus.Infof("downloaded store plugin %s from %s to %s", storeNameStr, source.Artifact, targetPath)
+		} else {
+			logrus.Warnf("%s was specified for store plugin %s, but dynamic plugins are currently disabled", types.Source, storeNameStr)
+		}
+	}
+
 	storeFactory, ok := builtInStores[storeNameStr]
 	if ok {
 		return storeFactory.Create(configVersion, storeConfig)
diff --git a/pkg/referrerstore/types/types.go b/pkg/referrerstore/types/types.go
index 4296bd6..e5ea55b 100644
--- a/pkg/referrerstore/types/types.go
+++ b/pkg/referrerstore/types/types.go
@@ -27,6 +27,7 @@ const (
 	SpecVersion string = "0.1.0"
 	Version     string = "version"
 	Name        string = "name"
+	Source      string = "source"
 )
 
 const (
diff --git a/pkg/verifier/factory/factory.go b/pkg/verifier/factory/factory.go
index 46101f5..d7f6720 100644
--- a/pkg/verifier/factory/factory.go
+++ b/pkg/verifier/factory/factory.go
@@ -19,8 +19,11 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"path"
 	"strings"
 
+	pluginCommon "github.com/deislabs/ratify/pkg/common/plugin"
+	"github.com/deislabs/ratify/pkg/featureflag"
 	"github.com/deislabs/ratify/pkg/verifier"
 	"github.com/deislabs/ratify/pkg/verifier/config"
 	"github.com/deislabs/ratify/pkg/verifier/plugin"
@@ -58,6 +61,25 @@ func CreateVerifierFromConfig(verifierConfig config.VerifierConfig, configVersio
 		return nil, fmt.Errorf("invalid plugin name for a verifier: %s", verifierNameStr)
 	}
 
+	// if source is specified, download the plugin
+	if source, ok := verifierConfig[types.Source]; ok {
+		if featureflag.DynamicPlugins.Enabled {
+			source, err := pluginCommon.ParsePluginSource(source)
+			if err != nil {
+				return nil, fmt.Errorf("failed to parse plugin source: %w", err)
+			}
+
+			targetPath := path.Join(pluginBinDir[0], verifierNameStr)
+			err = pluginCommon.DownloadPlugin(source, targetPath)
+			if err != nil {
+				return nil, fmt.Errorf("failed to download plugin: %w", err)
+			}
+			logrus.Infof("downloaded verifier plugin %s from %s to %s", verifierNameStr, source.Artifact, targetPath)
+		} else {
+			logrus.Warnf("%s was specified for verifier plugin %s, but dynamic plugins are currently disabled", types.Source, verifierNameStr)
+		}
+	}
+
 	verifierFactory, ok := builtInVerifiers[verifierNameStr]
 	if ok {
 		return verifierFactory.Create(configVersion, verifierConfig)
diff --git a/pkg/verifier/types/types.go b/pkg/verifier/types/types.go
index 6587adc..db98ae2 100644
--- a/pkg/verifier/types/types.go
+++ b/pkg/verifier/types/types.go
@@ -28,6 +28,7 @@ const (
 	Name             string = "name"
 	ArtifactTypes    string = "artifactTypes"
 	NestedReferences string = "nestedReferences"
+	Source           string = "source"
 )
 
 const (


Test patch:
diff --git a/pkg/common/plugin/download_test.go b/pkg/common/plugin/download_test.go
new file mode 100644
index 0000000..5abc3b8
--- /dev/null
+++ b/pkg/common/plugin/download_test.go
@@ -0,0 +1,75 @@
+/*
+Copyright The Ratify Authors.
+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 plugin
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/deislabs/ratify/api/v1alpha1"
+)
+
+func TestParsePluginSource_HandlesJSON(t *testing.T) {
+	js := `{
+	"name": "dynamic",
+	"artifactTypes": "sbom/example",
+	"nestedReferences": "application/vnd.cncf.notary.signature",
+	"source": {
+			"artifact": "wabbitnetworks.azurecr.io/test/sample-plugin:v1",
+			"authProvider": {
+				"name": "dockerConfig"
+			}
+	}
+}`
+
+	var verifierConfig map[string]interface{}
+	err := json.Unmarshal([]byte(js), &verifierConfig)
+	if err != nil {
+		t.Fatalf("failed to unmarshal verifier config: %v", err)
+	}
+
+	source, err := ParsePluginSource(verifierConfig["source"])
+	if err != nil {
+		t.Fatalf("failed to parse plugin source: %v", err)
+	}
+
+	if source.Artifact != "wabbitnetworks.azurecr.io/test/sample-plugin:v1" {
+		t.Fatalf("unexpected artifact: %s", source.Artifact)
+	}
+
+	if source.AuthProvider["name"] != "dockerConfig" {
+		t.Fatalf("unexpected auth provider: %s", source.AuthProvider["name"])
+	}
+}
+
+func TestParsePluginSource_HandlesCRD(t *testing.T) {
+	verifierConfig := v1alpha1.VerifierSpec{
+		Name:          "dynamic",
+		ArtifactTypes: "sbom/example",
+		Source: &v1alpha1.PluginSource{
+			Artifact: "wabbitnetworks.azurecr.io/test/sample-plugin:v1",
+		},
+	}
+
+	source, err := ParsePluginSource(verifierConfig.Source)
+	if err != nil {
+		t.Fatalf("failed to parse plugin source: %v", err)
+	}
+
+	if source.Artifact != "wabbitnetworks.azurecr.io/test/sample-plugin:v1" {
+		t.Fatalf("unexpected artifact: %s", source.Artifact)
+	}
+}
diff --git a/test/bats/cli-test.bats b/test/bats/cli-test.bats
index 120dda1..237937b 100644
--- a/test/bats/cli-test.bats
+++ b/test/bats/cli-test.bats
@@ -47,3 +47,31 @@ load helpers
     run bin/ratify verify -c $RATIFY_DIR/config.json -s wabbitnetworks.azurecr.io/test/all-in-one-image:signed
     assert_cmd_verify_success
 }
+
+@test "dynamic plugin verifier test" {
+    # dynamic plugins disabled by default
+    run bash -c "bin/ratify verify -c $RATIFY_DIR/dynamic_plugins_config.json -s wabbitnetworks.azurecr.io/test/all-in-one-image:signed 2>&1 >/dev/null | grep 'dynamic plugins are currently disabled'"
+    assert_success
+
+    # dynamic plugins enabled with feature flag
+    run bash -c "RATIFY_DYNAMIC_PLUGINS=1 bin/ratify verify -c $RATIFY_DIR/dynamic_plugins_config.json -s wabbitnetworks.azurecr.io/test/all-in-one-image:signed 2>&1 >/dev/null | grep 'downloaded verifier plugin dynamic from .* to .*'"
+    assert_success
+
+    # ensure the plugin is downloaded and marked executable
+    test -x $RATIFY_DIR/plugins/dynamic
+    assert_success
+}
+
+@test "dynamic plugin store test" {
+    # dynamic plugins disabled by default
+    run bash -c "bin/ratify verify -c $RATIFY_DIR/dynamic_plugins_config.json -s wabbitnetworks.azurecr.io/test/all-in-one-image:signed 2>&1 >/dev/null | grep 'dynamic plugins are currently disabled'"
+    assert_success
+
+    # dynamic plugins enabled with feature flag
+    run bash -c "RATIFY_DYNAMIC_PLUGINS=1 bin/ratify verify -c $RATIFY_DIR/dynamic_plugins_config.json -s wabbitnetworks.azurecr.io/test/all-in-one-image:signed 2>&1 >/dev/null | grep 'downloaded store plugin dynamicstore from .* to .*'"
+    assert_success
+
+    # ensure the plugin is downloaded and marked executable
+    test -x $RATIFY_DIR/plugins/dynamicstore
+    assert_success
+}
diff --git a/test/bats/test.bats b/test/bats/test.bats
index 54ce958..71768db 100644
--- a/test/bats/test.bats
+++ b/test/bats/test.bats
@@ -209,6 +209,48 @@ SLEEP_TIME=1
     wait_for_process ${WAIT_TIME} ${SLEEP_TIME} "kubectl replace --namespace=gatekeeper-system -f currentConfig.yaml"
 }
 
+@test "dynamic plugins disabled test" {
+    teardown() {
+        wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete verifiers.config.ratify.deislabs.io/verifier-dynamic --namespace default --ignore-not-found=true'
+    }
+
+    start=$(date --iso-8601=seconds)
+    latestpod=$(kubectl -n gatekeeper-system get pod -l=app.kubernetes.io/name=ratify --sort-by=.metadata.creationTimestamp -o=name | tail -n 1)
+
+    run kubectl apply -f ./config/samples/config_v1alpha1_verifier_dynamic.yaml
+    sleep 5
+
+    run bash -c "kubectl -n gatekeeper-system logs $latestpod --since-time=$start | grep 'dynamic plugins are currently disabled'"
+    assert_success
+}
+
+@test "dynamic plugins enabled test" {
+    # only run this test against a live cluster
+    if [[ -z "${AKS_NAME}" ]]; then
+        skip "helm upgrade fails in the pipeline on a 2-core runner"
+    fi
+
+    # ensure that the chart deployment is reset to a clean state for other tests
+    teardown() {
+        wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete verifiers.config.ratify.deislabs.io/verifier-dynamic --namespace default --ignore-not-found=true'
+        pod=$(kubectl -n gatekeeper-system get pod -l=app.kubernetes.io/name=ratify --sort-by=.metadata.creationTimestamp -o=name | tail -n 1)
+        helm upgrade --atomic --namespace gatekeeper-system --reuse-values --set featureFlags.RATIFY_DYNAMIC_PLUGINS=false ratify ./charts/ratify
+        wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl -n gatekeeper-system delete $pod --force --grace-period=0'
+    }
+
+    # enable dynamic plugins
+    helm upgrade --atomic --namespace gatekeeper-system --reuse-values --set featureFlags.RATIFY_DYNAMIC_PLUGINS=true ratify ./charts/ratify
+    sleep 5
+    latestpod=$(kubectl -n gatekeeper-system get pod -l=app.kubernetes.io/name=ratify --sort-by=.metadata.creationTimestamp -o=name | tail -n 1)
+
+    run kubectl apply -f ./config/samples/config_v1alpha1_verifier_dynamic.yaml
+    sleep 5
+
+    # parse the logs for the newly created ratify pod
+    run bash -c "kubectl -n gatekeeper-system logs $latestpod | grep 'downloaded verifier plugin dynamic from .* to .*'"
+    assert_success
+}
+
 @test "validate mutation tag to digest" {
     run kubectl apply -f ./library/default/template.yaml
     assert_success
diff --git a/test/bats/tests/config/dynamic_plugins_config.json b/test/bats/tests/config/dynamic_plugins_config.json
new file mode 100644
index 0000000..fd0b0db
--- /dev/null
+++ b/test/bats/tests/config/dynamic_plugins_config.json
@@ -0,0 +1,63 @@
+{
+    "store": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "oras",
+                "cosignEnabled": true
+            },
+            {
+                "name": "dynamicstore",
+                "source": {
+                    "artifact": "wabbitnetworks.azurecr.io/test/sample-store-plugin:v1"
+                }
+            }
+        ]
+    },
+    "policy": {
+        "version": "1.0.0",
+        "plugin": {
+            "name": "configPolicy"
+        }
+    },
+    "verifier": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "dynamic",
+                "artifactTypes": "sbom/example",
+                "nestedReferences": "application/vnd.cncf.notary.signature",
+                "source": {
+                    "artifact": "wabbitnetworks.azurecr.io/test/sample-verifier-plugin:v1"
+                }
+            },
+            {
+                "name": "notaryv2",
+                "artifactTypes": "application/vnd.cncf.notary.signature",
+                "verificationCerts": [
+                    "~/.ratify/ratify-certs/notary/wabbit-networks.io.crt"
+                ],
+                "trustPolicyDoc": {
+                    "version": "1.0",
+                    "trustPolicies": [
+                        {
+                            "name": "default",
+                            "registryScopes": [
+                                "*"
+                            ],
+                            "signatureVerification": {
+                                "level": "strict"
+                            },
+                            "trustStores": [
+                                "ca:certs"
+                            ],
+                            "trustedIdentities": [
+                                "*"
+                            ]
+                        }
+                    ]
+                }
+            }
+        ]
+    }
+}