Skip to main content
Version: 1.3

Framework Overview

This specification outlines a workflow of artifact verification which can be utilized by artifact consumers and provides a framework for the Notary project to be consumed by clients that consume these artifacts. The RATIFY framework enables composition of various verifiers and referrer stores to build verification framework into projects like GateKeeper. This framework serves as an orchestrator of various verifiers that are coordinated to get a consolidated verification result using a policy. This verification result can be used to make decisions in admission controllers at different stages like build, deployment or runtime like Kubernetes using OPA Gatekeeper.

This is a DRAFT specification and is a guidance for the prototype.

The framework defines concepts involved in the verification process and provides interfaces to them. The main concepts are referrer store, referrer provider, reference verifier , worlflow executor & policy engine. It uses plugin architecture to implement some of these concepts and thereby enables extensibility and interoperability to support both the existing and new emerging needs of the artifact verification problem. The different sections of this document describes each of these concepts in detail.

Glossary

  • Subject: The artifact under verification usually identified by a reference as per the OCI {DNS/IP}/{Repository}:[name|digest].
  • Reference Type: A new type of OCI artifact to a registry that is referencing existing, unchanged, content.
  • Referrer: The reference artifact identified by a reference as per the OCI {DNS/IP}/{Repository}:[name|digest] and references a subject.
  • Plugin: Code packages that provide a specific functionailty of the framework. Each plugin adheres to a specification and are designed for interoperability.
  • Store : An artifact store that can store and distribute OCI artifacts with the ability to discover reference types for a subject.
  • Verifier: Code package responsible for the verification of a particular artifact type(s)
  • Configuration: The set of parameters to configure the framework.
  • Policy: A set of rules to make various decisions in the process of artifact verification.

Provider architecture

This framework will use a provider model for extensibility to support different types of stores & verifiers. It supports two types of providers.

Built-In/Internal providers are available in the source of the framework and are registered during startup using the init function. Referrer store using ORAS and signature verification using notation will be available as the built-in providers that are managed by the framework.

External/Plugin providers are external to the source and process of the framework. These providers will be registered as binaries that the framework will locate in the configured paths and execute as per the corresponding plugin specification. The following section outlines the plugin architecture used for supporting external providers into the framework.

Plugin architecture

This framework will use a plugin architecture to integrate custom stores & verifiers. Individual plugins can be registered by declaring them in the framework's configuration.

Requirements

  • The functional and error specification for each of the plugin types MUST be defined by the framework
      • The data exchange contract SHOULD be versioned to support future changes and compatibility.
  • The plugin SHOULD be an executable/binary that will be executed by the framework with appropriate environment variables and other data through stdin. This data exchange contract MUST be defined in the plugin specification provided by the framework.
  • The initial version of the framework allows plugins to be binaries. Future versions of the framework MAY support plugins that are services invoked through gRPC or HTTP.
  • The plugins MUST be registered in the framework's configuration file. When registered, they can have custom settings that are specific to that plugin. A set of reserved keys like name will be used by the framework. The framework MUST pass through other fields, unchanged to the plugin at the time of execution.
  • The location of these plugin binaries MAY be defined as part of registration. However, if not provided, the default path ${HOME}/.ratify/plugins will be used as the path to search for the plugins.
  • The framework MUST validate the existence of plugin binaries in the appropriate paths at the start up and fail with error if any of the registered plugins are not found in the configured paths.
  • [TODO] Permissions and threat model for executing these plugins SHOULD be specified.
  • In addition to the specification, the framework SHOULD provide libraries for writing plugins. A simple CLI for example ratify plugin verifier add myverifier to create a stub for a plugin using these libraries MAY be provided by the framework.

Open Questions

  • How do we handle version incompatibility issues? Do we need to add a version check API for every plugin that will be used by the framework to determine the compatibility?

Artifact references

An artifact is defined by a manifest and identified by a reference as per the OCI {DNS/IP}/{Repository}:[name|digest]. A reference artifact is linked to another artifact through descriptor and enables forming a chain of artifacts as shown below. An artifact that has reference types is termed as subject against which verification can be triggered using this framework.

tree view of artifact hierarchy

This new way of defining reference types to artifacts and creating relationships is described in the spec. This framework queries for this graph of reference types artifacts for a subject to verify it. For existing systems that do not support these reference types, there will be storage wrappers that can adapt the corresponding supply chain objects as reference types. The advantage is to have a uniform data format that can be consumed by different components of the framework.

Example

Cosign signatures are stored as specially formatted tags that are queried using cosign libraries during verification. There will be an adapter that can represent these signatures as the reference types on fly if exists, so that it can be consumed by the cosign verifier that is registered in the framework.

Referrer Store

A referrer store is an interface that defines the following capabilities

  • Retrieve list of referrers for an artifact
  • Retrieve manifest for a referrer identified by the OCI reference
  • Download the blobs of an artifact
  • Retrieves properties of the subject manifest like descriptor to support verification.

Sample Interface

type ReferrerStore interface {
// Name is the name of the store
Name() string

// ListReferrers returns the immediate set of supply chain objects for the given subject
// represented as artifact manifests
ListReferrers(ctx context.Context, subjectReference common.Reference, artifactTypes []string, nextToken string, subjectDesc *ocispecs.SubjectDescriptor) (ListReferrersResult, error)

// GetBlobContent returns the blob with the given digest
// WARNING: This API is intended to use for small objects like signatures, SBoMs
GetBlobContent(ctx context.Context, subjectReference common.Reference, digest digest.Digest) ([]byte, error)

// GetReferenceManifest returns the reference artifact manifest as given by the descriptor
GetReferenceManifest(ctx context.Context, subjectReference common.Reference, referenceDesc ocispecs.ReferenceDescriptor) (ocispecs.ReferenceManifest, error)

// GetConfig returns the configuration of this store
GetConfig() *config.StoreConfig

// GetSubjectDescriptor returns the descriptor for the given subject.
GetSubjectDescriptor(ctx context.Context, subjectReference common.Reference) (*ocispecs.SubjectDescriptor, error)
}

Requirements

  • A referrer store SHOULD be a plugin that implements the interface. The plugin will be integrated in the framework using registration through configuration.
  • Any specific settings required for a store SHOULD be embedded in the plugin configuration that is part of the framework. These settings SHOULD not conflict with reserved properties defined by the framework.
  • There is no ordinal sequencing for the referrers returned by the store.
  • The framework MAY provide a reference implementation for the interface as a builtin plugin. For example, store implementation using OCI registry APIs.

Open Questions

  • If there are no referrers available for a subject, should it return an error or an empty result?
  • Can we assume that the size of the blobs contained within a reference is manageable to download in memory and pass it to the framework? If so, what will be the size limit? Or do we need to support streaming of the content?

Referrer Store Plugin and Framework contract specification

Referrer/Reference Provider

The framework can be configured with multiple referrer stores. A referrers provider provides a capability to query one or more underlying referrer stores to obtain a list of possible refererence artifacts of a particular artifact type. Given an artifact, a set of registered stores will be queried to retrieve all the references. Every reference will be associated with the details of the store it is retrieved from. This can be used to query further metadata related to a reference like manifest & blobs as part of verifying it.

Requirements

  • Multiple stores MAY be registered through configuration as plugins or internal providers.
  • Provider SHOULD ensure that there is at least ONE store registered in the configuration.
  • The order of query for references by the provider MAY be governed by order of registrations of stores in the configuration.
  • Selection of a particular referrers store is defined below. The same can be used for an artifact to restrict the query to a particular store.

MatchingLables might become the construct that we chose to detemine which stores to query for references.

  • Order or selection does not ensure order of return of the references and there will be no ordinal sequencing of references retruned from the provider.

Reference Verifier

A reference verifier is an interface that defines the following capabilities

  • Given an reference type artifact, determine if the verifier supports its verification like CanVerify
  • Verifies the reference artifact and returns the result of verification.

Sample Interface

type ReferenceVerifier interface {
Name() string
CanVerify(ctx context.Context, referenceDescriptor ocispecs.ReferenceDescriptor) bool
Verify(ctx context.Context,
subjectReference common.Reference,
referenceDescriptor ocispecs.ReferenceDescriptor
) (VerifierResult, error)
}

Requirements

  • A verifier SHOULD either be an internal provider or a plugin that implements the interface. The plugin will be integrated in the framework using registration through configuration.
  • Any specific settings required for a verifier SHOULD be embedded in the plugin configuration that is part of the framework. These settings SHOULD not conflict with reserved properties defined by the framework.
  • For a given artifact type, all verifiers that CanVerify will be invoked in the order that are registered in the configuration.
  • The verifier SHOULD return a result that contains all details of the verification for auditing purposes.
  • Every verifier should have access to the store where the reference artifact is queried from. This store is used to fetch other metadata required for verification like manifest & blobs.
  • The framework MAY provide reference implementations for the interface as internal providers. For example notation verifier that implements the verification of artifacts with type notary.signature. Similary there can be SBOM verifier that supports verification of SBOMs.

Open Questions

  • Do we want to use matchinCriteria model for determining the verifiers for a given artifact type?
  • Do we want the support of filtering by annotations?

Reference Verifier Plugin & Framework contract specification

Executor

The executor is responsible for composing multiple verifiers with multiple stores to create a workflow of verification for an artifact. For a given subject, it fetches all referrers from multiple stores and ensures multiple verifiers can be chained and executed one after the other. It is also responsible for handling nested verification as required for some artifact types.

Sample Interface

type VerifyParameters struct {
Subject string `json:"subjectReference"`
ReferenceTypes []string `json:"referenceTypes,omitempty"`
}

type Executor interface {
VerifySubject(ctx context.Context, verifyParameters VerifyParameters) (types.VerifyResult, error)
}

Requirements

  • The framework MUST provide a reference implementation for an executor interface. This will be the default executor in the framework. The initial version will not support plugin model for an executor but MAY be added in future.
  • An executor composes multiple verifiers and a reference provider with underlying multiple stores.
  • For a given artifact type, all verifiers that support it's verification, will be invoked in the order that are registered in the configuration.
  • Verifiers MAY be registered with the executor dynamically at runtime.
  • The executor MAY invoke the verifiers synchronously.
  • The executor MAY use a cache to optimize the performance of repeated workflow executions for a given subject. The executor configuration SHOULD allow for disabling this cache if needed and also support setting other parameters like cache size & eviction polices.

Nested Verification

For some artifact types, nested verification MAY be required for hierarchical verification. For e.g. when verifying an SBOM, we first need to ensure that the attestation for SBOM is validated before validating the actual SBOM.

IMAGE
└── SBOM
└── SIGNATURE

There could be a tree of references that needs to be traversed and verified before verifying a reference artifact. Verification of nested references creates a recursive process of verification within the workflow. The final verification result should include the results of each and every verifier that is invoked as part of nested verification.

Sample Data Flow for executor

Sample Data flow with gatekeeper

Open Questions

  • Can verifiers that are selected for an artifact type be invoked in parallel through some setting in the configuration?
  • Should exception hanlder be a different handler or just another verifier (TBD)
  • Should executor or verifier responsible for nested verification?
  • Given the recursive nature of verifying the nested references, do we need to define an exit criteria to limit the nested levels?
  • How do we define the need for nested verification for a given artifact type? Will this be encompassed within the verifier settings or will it be a separate config?
  • Can nested verification be the default behavior for every artifact with an option to exclude this behavior for some artifacts? For example, all artifacts will perform nested verification except for the notation signatures?

Models of executor

There are two different models of execution for the artifact verification.

Model 1: Standalone verifiers

In this model, every verifier that is registered with the framework is standalone and is responsible for complete verification process including query of references using the store. The executor iterates through the registered verifiers in the given order and invokes each of them for verification of the subject. Every verifier will be invoked with the plugins configuration for the stores so that they can create a provider that helps with querying the reference types.

  • A verifier can choose to query the related supply chain objects in any format without the need for them to be represented as reference types. This allows for removing dependency on the artifacts specification.
  • A verifier will also be responsible for nested verification of its artifacts as needed.

Pros

  • A verifier can choose to query the related supply chain objects in any format without the need for them to be represented as reference types. This allows for removing dependency on the artifacts spec.

Cons

  • This model will be good if registry supports artifact type filtering. If not, every verifier will query all referrers and do the filtering on the client side which can be expensive (we have hard 3 second timeout for the admission controller in k8s)
  • How do we manage configuration without duplicating? For example, trust policy required to verify signatures of SBOM might be duplicated both under SBOM and notation verifier. If delegation model is used to support nested verification, then model 2 might be cleaner way to control the flow of verification.

Model 2: Light weight verifiers

In this model, a verifier is only responsible for the verification of a reference. The executor will query for the references and walk them in the order of the registered verifiers. Some notable points are

  • This model will be tightly coupled with the artifact spec where all supply chain objects are represented as reference types. This will need wrappers to support existing systems like cosign and TUF
  • The executor should support defining nested references for a verifier that can help with hierarchical verification.

Pros

  • The stores are queried only once by the executor rather than by every verifier.
  • Simple configuration with no need of duplication.
  • Light weight plugin execution without the need for complex delegations.

Cons

  • This model is tightly coupled with artifact specs model where everything is represented as the references.
  • Nested verification should be carefully defined and executed.

Executor Policy Specification

The executor policy determines the chained outcome of execution of the multiple verifiers through the executor. The workflow with multiple stores & verifiers has different points of decisions that can be resolved using a policy.

Policy Engine

The framework will define interface for different decisions that govern the executor flow. A policy engine can implement this interface using different methodologies. It can be a simple configuration based engine that evaluates these decisions using the configuration parameters. For more advanced scenarios, the engine can offload the decision making to OPA.

Break Glass

For any kind of policy enforcement, it is recommended to do have dynamic overrides to support break glass scenarios in case of emergencies. This can be achieved by dynamically updating the policies for an engine at any point in time.

Common Decisions using Policies

  • Is verify needed for this artifact?
  • Is verify needed for this reference of an artifact?
  • Is this verifier in the workflow needed for this reference or artifact?
  • Continue with the next step/task in the workflow? This covers decisions around ignoring any/specific failures from verifiers or stores
  • Determining the final outcome of the verification using the results from multiple verifiers.

For each of the decision queries, the following sections gives some examples of how to evaluate it using either configuration or OPA based policy engines.

Query 1: Is verify needed for this artifact?

This query can filter artifacts for which verification has to be triggered or skipped.

Example

As an incremental strategy of adopting container security, teams can choose to verify images from their private registries like ACR only.

  • Configuration
policy:
type: config
artifactMatchingLabels: ["*.azurecr.io"]

OPA

policy:
type: config
rego: |
package ratify.rules

verify_artifact{
regex.match(".+.azurecr.io$", input.subject)
}

Query 2: Is verify needed for this reference of an artifact?

This query can filter references to artifacts based on the current state of the workflow and any other matching conditions.

Example

When an artifact has multiple signatures, teams can choose to ensure any one of them is valid. If the verification of one signature is success, teams can configure a policy to skip verification of all other signatures.

  • Configuration
policy:
type: config
skipAfterFirstSuccess: ["org.cncf.notary"]
  • OPA
policy:
type: config
rego: |
package ratify.rules

verify_reference{
not input.reference.artifactType == "org.cncf.notary"
}

verify_reference {
not notation_success
}

notation_success{
result := input.results[_]
result.artifactType == input.reference.artifactType
result.isSuccess == "true"
}

Query 3: Is this verifier in the workflow needed for this reference or artifact?

This query can help with customizing the verifiers in the workflow for a given reference or artifact.

Example

Teams can choose to skip vulnerability scan verifier for images from a private container registry but would like to enforce it for public registries like MCR.

  • Configuration
policy:
type: config
skipVerifiers:
- name : "scan"
matchingArtifacts: ["*.azurecr.io"]
  • OPA
policy:
type: config
rego: |
package ratify.rules

default skip_verifier = false

skip_verifier{
input.verifier.name == "scan"
regex.match("*.azurecr.io", input.subject)
}

Query 4: Continue with the next step/task in the workflow? This covers decisions around ignoring any/specific failures from verifiers or stores

This query can help with customizing the flow of the workflow for a given reference or artifact based on multiple conditions.

Example 1

Teams can choose to continue verification even if certain verifiers fails with certain types(or all) of errors especially during testing or break glass scenarios.

  • Configuration
policy:
type: config
continueOnErrors:
- name : "notation"
errorCodes: ["CERT_EXPIRED"]
matchingArtifacts: [*.azurecr.io]
  • OPA
policy:
type: config
rego: |
package ratify.rules

continue_workflow {
not any_failed
}
continue_workflow {
is_cert_expired
}
any_failed {
input.results[_].isSuccess == "false"
}

is_cert_expired {
regex.match(".+.azurecr.io$", input.subject)
result := input.results[_]
input.verifier.name == "notation"
input.verifier.artifactTypes[_] == result.artifactType
result.error.code == "CERT_EXPIRED"
}

Example 2

Teams usually configure verifiers for different reference types. To invoke a verifier, the executor queries for the artifacts of type(s) that are configured for that verifier. If no artifacts of that type exist in the store, the executor can throw a well defined error REFERRERS_NOT_FOUND for example. By default, when a verifier is configured with the framework, the executor ensures that the corresponding artifact type should be present in the store else it fails the verification. However, teams can choose to ignore the absence of certain artifact types for a subject through a policy.

Configuration

policy:
type: config
continueOnErrors:
- name : "sbom"
errorCodes: ["REFERRERS_NOT_FOUND"]
matchingArtifacts: [*.azurecr.io]

OPA

policy:
type: config
rego: |
package ratify.rules

continue_workflow {
not any_failed
}
continue_workflow {
is_SBOMs_not_found_ACR
}
any_failed {
input.results[_].isSuccess == "false"
}

is_SBOMs_not_found_ACR {
regex.match(".+.azurecr.io$", input.subject)
result := input.results[_]
input.verifier.name == "sbom"
input.verifier.artifactTypes[_] == result.artifactType
result.error.code == "REFERRERS_NOT_FOUND"
}

Query 5: Determining the final outcome of the verification using the results from multiple verifiers

This query will help with consolidating the outcomes from all verifiers to an aggregated result that will be used in admission controllers

Example

Teams can choose to continue with k8s deployment even if the outcomes from certain verifiers are not successful. This is useful during experimentation or development phase and can also help with break glass scenarios. The below policy evaluates the final outcome as success even where there are errors from a particular verifier newverifier

  • Configuration
policy:
type: config
ignoreFailuresForVerifiers : ["newverifier"]
  • OPA
policy:
type: config
rego: |
package ratify.rules

final_verification_success{
not any_failed
}

final_verification_success{
not other_verifier_failure
}

any_failed {
input.results[_].isSuccess == "false"
}

other_verifier_failure{
some i
input.results[i].isSuccess == "false"
input.results[i].name != "newverifier"
}

Framework Configuration

The configuration for the framework includes multiple sections that allows configuring referrer stores, verifiers, executor and policy engine.

Referrer Store Configuration

The section stores encompasses the registration of multiple referrer stores to the framework. It includes two main properties

PropertyTypeIsRequiredDescription
versionstringtrueThe version of the API contract between the framework and the plugin.
pluginsarraytrueThe array of store plugins that are registered with the framework.

A store will be registered as a plugin with the following configuration parameters

PropertyTypeIsRequiredDescription
namestringtrueThe name of the plugin
pluginBinDirsarrayfalseThe list of paths to look for the plugin binaries to execute. Default: the home path of the framework.

Any other parameters specified for a plugin other than the above mentioned are considered as opaque and will be passed to the plugin when invoked.

Verifier Configuration

The section verifiers encompasses the registration of multiple verifiers to the framework. It includes two main properties

PropertyTypeIsRequiredDescription
versionstringtrueThe version of the API contract between the framework and the plugin.
pluginsarraytrueThe array of verifier plugins that are registered with the framework.

A verifier will be registered as a plugin with the following configuration parameters

PropertyTypeIsRequiredDescription
namestringtrueThe name of the plugin
pluginBinDirsarrayfalseThe list of paths to look for the plugin binary to execute. Default: the home path of the framework.
artifactTypesarraytrueThe list of artifact types for which this verifier plugin has to be invoked. [TBD] May change to matchingLabels
nestedReferencesarrayfalseThe list of artifact types for which this verifier should initiate nested verification. [TBD] This is subject to change as it is under review

Any other parameters specified for a plugin other than the above mentioned are considered as opaque and will be passed to the plugin when invoked.

Executor Configuration

The section executor defines the configuration of the framework executor component.

PropertyTypeIsRequiredDescription
cacheboolfalseDefault: false. Determines if in-memory cache can be used to cache the executor outcomes for an artifact.
cacheExpirystringfalseDefault: [TBD]. Determines the TTL for the executor cache item.

Policy Engine Configuration

The section policy defines the configuration of the policy engine used by the framework.

Open Questions

  • Do we need to support plugin model?
  • Do we provide OPA by default?
  • How do we combine multiple policies?

Sample Configuration

stores:
version: 1.0.0
plugins:
- name: ociregistry
useHttp: true
verifiers:
version: 1.0.0
plugins:
- name: notation
artifactTypes: application/vnd.cncf.notary.signature
verificationCerts:
- "/home/user/.notary/keys/wabbit-networks.crt"
- name: sbom
artifactTypes: application/x.example.sbom.v0
nestedReferences: application/vnd.cncf.notary.signature
executor:
cache: false
policy:
type: opa
policy: |
package ratify.rules

verify_artifact{
regex.match(".+.azurecr.io$", input.subject)
}