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 HORA 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.
{DNS/IP}/{Repository}:[name|digest]
.{DNS/IP}/{Repository}:[name|digest]
and references a subject.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 notaryv2 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.
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.
stdin
. This data exchange contract MUST be defined in the plugin specification provided by the framework.name
will be used by the framework. The framework MUST pass through other fields, unchanged to the plugin at the time of execution.hora plugin verifier add myverifier
to create a stub for a plugin using these libraries MAY be provided by the framework.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.
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.
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 reference type on fly if exists, so that it can be consumed by the cosign verifier that is registered in the framework.
A referrer store is an interface that defines the following capabilities
type ReferrerStore interface {
Name() string
ListReferrers(ctx context.Context, subjectReference common.Reference, artifactTypes []string, nextToken string) (ListReferrersResult, error)
// Used for small objects.
GetBlobContent(ctx context.Context, subjectReference common.Reference, digest digest.Digest) ([]byte, error)
GetReferenceManifest(ctx context.Context, subjectReference common.Reference, referenceDesc ocispecs.ReferenceDescriptor) (ocispecs.ReferenceManifest, error)
}
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.
MatchingLables
might become the construct that we chose to detemine which stores to query for references.
type ReferrerProvider interface {
ListAllReferrers(ctx context.Context, subjectReference common.Reference, artifactTypes []string, nextToken string) (ListAllReferrersResult, error)
}
manifest
within in it.A reference verifier is an interface that defines the following capabilities
CanVerify
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)
}
CanVerify
will be invoked in the order that are registered in the configuration.notary.v2.signature
. Similary there can be SBOM verifier that supports verification of SBOMs.matchinCriteria
model for determining the verifiers for a given artifact type?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.
type VerifyParameters struct {
Subject string `json:"subjectReference"`
ReferenceTypes []string `json:"referenceTypes,omitempty"`
}
type Executor interface {
VerifySubject(ctx context.Context, verifyParameters VerifyParameters) (types.VerifyResult, error)
}
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.
There are two different models of execution for the artifact verification.
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 reference provider. 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.
Pros
Cons
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
Pros
Cons
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.
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.
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.
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.
This query can filter artifacts for which verification has to be triggered or skipped.
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 hora.rules
verify_artifact{
regex.match(".+.azurecr.io$", input.subject)
}
This query can filter references to artifacts based on the current state of the workflow and any other matching conditions.
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
org.cncf.notary.v2: any
org.cncf.sbom.v2: all
OPA
policy:
type: config
rego: |
package hora.rules
verify_reference{
not input.reference.artifactType == "org.cncf.notary.v2"
}
verify_reference {
not notary_success
}
notary_success{
result := input.results[_]
result.artifactType == input.reference.artifactType
result.isSuccess == "true"
}
This query can help with customizing the verifiers in the workflow for a given reference or artifact.
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 hora.rules
default skip_verifier = false
skip_verifier{
input.verifier.name == "scan"
regex.match("*.azurecr.io", input.subject)
}
This query can help with customizing the flow of the workflow for a given reference or artifact based on multiple conditions.
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 : "notaryv2"
errorCodes: ["CERT_EXPIRED"]
matchingArtifacts: [*.azurecr.io]
OPA
policy:
type: config
rego: |
package hora.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 == "notaryv2"
input.verifier.artifactTypes[_] == result.artifactType
result.error.code == "CERT_EXPIRED"
}
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 abscense 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 hora.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"
}
This query will help with consolidating the outcomes from all verifiers to an aggregated result that will be used in admission controllers
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 hora.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"
}
The configuration for the framework includes multiple sections that allows configuring referrer stores, verifiers, executor and policy engine.
The section stores
encompasses the registration of multiple referrer stores to the framework. It includes two main properties
Property | Type | IsRequired | Description |
---|---|---|---|
version | string | true | The version of the API contract between the framework and the plugin. |
plugins | array | true | The array of store plugins that are registered with the framework. |
A store will be registered as a plugin with the following configuration parameters
Property | Type | IsRequired | Description |
---|---|---|---|
name | string | true | The name of the plugin |
pluginBinDirs | array | false | The 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.
The section verifiers
encompasses the registration of multiple verifiers to the framework. It includes two main properties
Property | Type | IsRequired | Description |
---|---|---|---|
version | string | true | The version of the API contract between the framework and the plugin. |
plugins | array | true | The array of verifier plugins that are registered with the framework. |
A verifier will be registered as a plugin with the following configuration parameters
Property | Type | IsRequired | Description |
---|---|---|---|
name | string | true | The name of the plugin |
pluginBinDirs | array | false | The list of paths to look for the plugin binary to execute. Default: the home path of the framework. |
artifactTypes | array | true | The list of artifact types for which this verifier plugin has to be invoked. [TBD] May change to matchingLabels |
nestedReferences | array | false | The 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.
The section executor
defines the configuration of the framework executor component.
Property | Type | IsRequired | Description |
---|---|---|---|
cache | bool | false | Default: false. Determines if in-memory cache can be used to cache the executor outcomes for an artifact. |
cacheExpiry | string | false | Default: [TBD]. Determines the TTL for the executor cache item. |
The section policy
defines the configuration of the policy engine used by the framework.
stores:
version: 1.0.0
plugins:
- name: ociregistry
useHttp: true
verifiers:
version: 1.0.0
plugins:
- name: nv2verifier
artifactTypes: application/vnd.cncf.notary.v2
verificationCerts:
- "/home/user/.notary/keys/wabbit-networks.crt"
- name: sbom
artifactTypes: application/x.example.sbom.v0
nestedReferences: application/vnd.cncf.notary.v2
executor:
cache: false
policy:
type: opa
policy: |
package hora.rules
verify_artifact{
regex.match(".+.azurecr.io$", input.subject)
}