Skip to content

b2_04_oci_interaction_

Simon Baird edited this page Apr 23, 2025 · 2 revisions

Chapter 4: OCI Interaction

In the previous chapter, Chapter 3: Output Reporting, we saw how ec-cli presents the results of its checks. But where does ec-cli get all the information about the container images it needs to check? How does it know the image's configuration, find its security signatures, or download its attestations like vulnerability scans?

This chapter introduces OCI Interaction, the mechanism ec-cli uses to communicate with container registries.

What Problem Does OCI Interaction Solve?

Imagine a vast warehouse filled with boxes. Each box contains a specific product (a container image). The warehouse has a system for finding boxes (e.g., quay.io/my-org/my-app:v1.0), checking their labels (signatures, attestations), and looking inside (image configuration, layers).

Container registries (like Docker Hub, Quay.io, Google Artifact Registry, etc.) are like these warehouses for software containers. They follow standards defined by the Open Container Initiative (OCI). When ec-cli needs to validate a container image specified in an Application Snapshot, it needs a way to:

  1. Locate the specific image "box" in the registry "warehouse".
  2. Retrieve information about the image (its configuration, build details).
  3. Find related artifacts like digital signatures or attestations (e.g., SLSA provenance, vulnerability scans) that might be stored alongside the image.
  4. Sometimes, even inspect the contents of the image layers to find specific files.

Manually doing this would be complex. ec-cli needs a built-in "liaison" to handle all communication with these OCI registries efficiently and reliably.

Our Goal: To understand how ec-cli interacts with OCI registries to fetch the necessary data (images, configurations, signatures, attestations) required for policy evaluation.

Key Concepts

Let's break down the main ideas related to OCI Interaction:

  1. OCI Registry (The Warehouse): A server or service that stores and distributes container images and related artifacts according to OCI standards. Examples: Docker Hub, Quay.io, GitHub Container Registry (GHCR), Google Artifact Registry (GAR).
  2. Container Image (The Box): The software package stored in the registry, identified by a name and usually a tag (like :v1.0) or a unique digest (like @sha256:...).
  3. Image Manifest (The Box Inventory List): A JSON file associated with an image tag or digest. It lists the image's configuration file and its layers (the actual filesystem parts). For multi-platform images, it might point to other manifests.
  4. Image Configuration (The Box Setup Instructions): Another JSON file linked from the manifest. It contains metadata about the image, like environment variables, default command to run, architecture (amd64, arm64), etc.
  5. Signatures & Attestations (Labels & Attached Documents): These are separate artifacts often stored in the registry near the image.
    • Signatures: Prove who created or approved the image (using tools like Sigstore Cosign).
    • Attestations: Provide verifiable claims about the image, like its build process (SLSA provenance) or vulnerability scan results (like a CycloneDX SBOM). ec-cli heavily relies on fetching these.
  6. Layers (The Contents): The building blocks of the container image, usually represented as compressed tarballs of filesystem changes. ec-cli might sometimes need to extract specific files from these layers for policy checks.

How ec-cli Uses OCI Interaction

You generally don't interact with the OCI components directly when using ec-cli's core validation commands. It happens automatically behind the scenes.

When you run:

ec validate image --image quay.io/my-org/my-app:v1.0 \
                  --policy my-org/standard-policy

ec-cli performs the following steps involving OCI interaction:

  1. Parse Image Reference: It understands quay.io/my-org/my-app:v1.0 refers to an image in the quay.io registry.
  2. Resolve Digest (Tag-to-Digest): Container image tags (like :v1.0) can be moved. For reliable verification, ec-cli often first asks the registry: "What is the unique, unchanging digest (sha256:...) currently pointed to by the tag v1.0 for quay.io/my-org/my-app?" This ensures it checks the exact version intended.
  3. Fetch Manifest & Config: It contacts the registry (quay.io) and requests the image manifest for the resolved digest. From the manifest, it finds and fetches the image configuration file. This information is needed for many policy rules (e.g., checking labels, base image).
  4. Fetch Signatures & Attestations: ec-cli (often using integrated libraries like Cosign) queries the registry for associated signatures and attestations (like SLSA provenance, SBOMs, vulnerability reports) linked to the image digest. These are crucial inputs for security policies. We'll learn more about this in Chapter 5: Sigstore Integration.
  5. Fetch Layers (If Needed): Some advanced policies might require checking for specific files within the image (e.g., checking configuration files, looking for specific license files). If needed, ec-cli can fetch the image layers and extract the required files.

All this fetched data (config, attestations, etc.) becomes part of the input fed into the Policy Definition & Evaluation engine.

Under the Hood: The Communication Flow

High-Level Steps:

  1. Input: ec-cli receives an image reference (e.g., quay.io/my-org/my-app:v1.0).
  2. Resolve: It potentially resolves the tag to a specific digest by querying the registry.
  3. Authenticate (if needed): If the registry requires login, ec-cli uses credentials (often configured via tools like docker login).
  4. Fetch Core Data: It uses OCI client libraries to make API calls to the registry to download the image manifest and configuration JSON.
  5. Fetch Ancillary Data: It uses libraries (like Cosign) to query the registry for signatures and various types of attestations associated with the image digest.
  6. Fetch Layers (Optional): If required by a policy, it fetches image layers.
  7. Provide Data: It packages all the fetched information for the policy evaluation engine.

Simplified Sequence Diagram:

sequenceDiagram
    participant User
    participant CLI as ec validate image
    participant OCIClient as OCI Client Libs
    participant Registry as OCI Registry (e.g., Quay.io)

    User->>CLI: Run command with image ref
    CLI->>OCIClient: Resolve image tag to digest (quay.io/my-org/my-app:v1.0)
    OCIClient->>Registry: API request: HEAD /v2/.../manifests/v1.0
    Registry-->>OCIClient: Return digest (sha256:abc...)
    CLI->>OCIClient: Fetch Manifest & Config for digest (sha256:abc...)
    OCIClient->>Registry: API request: GET /v2/.../manifests/sha256:abc...
    Registry-->>OCIClient: Return Manifest JSON
    OCIClient->>Registry: API request: GET /v2/.../blobs/<config_digest>
    Registry-->>OCIClient: Return Config JSON
    CLI->>OCIClient: Fetch Signatures/Attestations for digest (sha256:abc...)
    OCIClient->>Registry: API requests: Find related signature/attestation artifacts
    Registry-->>OCIClient: Return Signature/Attestation data
    CLI-->>User: (after policy evaluation) Show results
Loading

Diving into the Code (A Glimpse)

Let's look at simplified concepts from the codebase.

1. The OCI Client Interface (internal/utils/oci/client.go)

ec-cli uses an internal client abstraction to handle registry communication. This allows consistent handling of authentication, retries, and caching.

// Simplified concept from internal/utils/oci/client.go
package oci

import (
	// ... necessary imports ...
	"github.com/google/go-containerregistry/pkg/name"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/sigstore/cosign/v2/pkg/cosign"
	"github.com/sigstore/cosign/v2/pkg/oci"
)

// Client defines the interface for interacting with OCI registries
type Client interface {
	// Fetches image metadata (like digest) without pulling all data
	Head(name.Reference) (*v1.Descriptor, error)
	// Resolves a tag or ambiguous reference to a specific digest string
	ResolveDigest(name.Reference) (string, error)
	// Fetches the full image object (manifest, config, layers access)
	Image(name.Reference) (v1.Image, error)
	// Verifies signatures associated with the image reference
	VerifyImageSignatures(name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error)
	// Verifies attestations associated with the image reference
	VerifyImageAttestations(name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error)
	// Fetches a specific layer blob by its digest
	Layer(name.Digest) (v1.Layer, error)
	// ... other methods like Index() for image indexes ...
}

// NewClient creates a default OCI client instance, potentially using cached context
func NewClient(ctx context.Context, opts ...remote.Option) Client {
    // ... checks context for existing client ...

    // Configures options like authentication, retries, transport (HTTP client)
	defaultOpts := createRemoteOptions(ctx)
	// ... merge provided opts ...

	return &defaultClient{ctx, defaultOpts} // Returns the actual implementation
}

This code defines a standard Client interface for OCI operations. NewClient sets up the connection details, using libraries like go-containerregistry and cosign under the hood. Notice methods specifically for fetching images, signatures, and attestations.

2. Resolving Image Tags (internal/image/process.go)

Before fetching data, ec-cli often resolves a tag to a digest.

// Simplified concept from internal/image/process.go
package image

import (
	// ... imports ...
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	"github.com/enterprise-contract/ec-cli/internal/utils/oci" // Using our OCI client
)

// ParseAndResolve takes a URL string and returns an ImageReference with the digest resolved.
func ParseAndResolve(ctx context.Context, url string, opts ...name.Option) (*ImageReference, error) {
	ref, err := NewImageReference(url, opts...) // Parse the basic reference
	if err != nil { return nil, err }

	// If the reference only has a tag, we need to find its digest
	if ref.Digest == "" {
		// Use the OCI client to ask the registry for the digest
		client := oci.NewClient(ctx)
		digestStr, err := client.ResolveDigest(ref.Ref())
		if err != nil {
			return nil, fmt.Errorf("unable to resolve digest for %s: %w", url, err)
		}
		// Update the reference with the fetched digest
		ref, err = NewImageReference(fmt.Sprintf("%s@%s", ref.Repository, digestStr), opts...)
		// ... error handling ...
	}

	return ref, nil
}

// ImageReference holds parsed image details
type ImageReference struct {
	Repository string
	Digest     string
	Tag        string
	ref        name.Reference // The underlying library object
}

// ... NewImageReference constructor ...

This shows how ParseAndResolve uses the oci.Client's ResolveDigest method to query the registry and get the stable sha256:... identifier for a potentially mutable tag like :latest or :v1.0.

3. Fetching Image Configuration (internal/fetchers/oci/config/config.go)

Here's an example of fetching specific data – the image config file.

// Simplified concept from internal/fetchers/oci/config/config.go
package config

import (
	// ... imports ...
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/enterprise-contract/ec-cli/internal/utils/oci" // Using our OCI client
)

// FetchImageConfig retrieves the config JSON for an image from its OCI registry.
func FetchImageConfig(ctx context.Context, ref name.Reference) (json.RawMessage, error) {
	// Get an OCI client instance
	client := oci.NewClient(ctx)

	// Use the client to fetch the image object
	image, err := client.Image(ref)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch image %s: %w", ref.String(), err)
	}

	// Ask the image object for its config file content
	configFile, err := image.ConfigFile()
	if err != nil {
		return nil, fmt.Errorf("failed to get config file for %s: %w", ref.String(), err)
	}

	// Marshal the config part into raw JSON bytes
	configJSON, err := json.Marshal(configFile.Config)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal config for %s: %w", ref.String(), err)
	}

	return configJSON, nil
}

This demonstrates using the oci.Client to get an Image object and then extracting the ConfigFile data from it, which is then returned as raw JSON for policy evaluation. Fetching signatures and attestations follows a similar pattern using methods like VerifyImageSignatures and VerifyImageAttestations.

4. OCI Access from Rego Policies (internal/rego/oci/oci.go)

Interestingly, ec-cli allows Rego policies themselves to dynamically fetch additional data from OCI registries during evaluation using custom built-in functions.

// Simplified concept from internal/rego/oci/oci.go
package oci

import (
    // ... imports ...
	"github.com/open-policy-agent/opa/rego"
	"github.com/enterprise-contract/ec-cli/internal/utils/oci" // Uses the client internally!
)

// Example: A Rego function ec.oci.image_manifest("registry/image@sha256:...")
// that policies can call.
func ociImageManifest(bctx rego.BuiltinContext, refTerm *ast.Term) (*ast.Term, error) {
	// ... parse input refTerm to get image URI string ...
	uri := string(uriValue)

	// Get an OCI client instance from the context
	client := oci.NewClient(bctx.Context)

    // Resolve if needed (tag -> digest)
	resolvedUri, err := resolveIfNeeded(client, uri)
	// ... handle error (return nil, policies see it as undefined) ...

	ref, _ := name.NewDigest(resolvedUri)
	// ... handle error ...

	// Use the client to fetch the image
	image, err := client.Image(ref)
	// ... handle error (return nil) ...

	// Get the manifest structure
	manifest, err := image.Manifest()
	// ... handle error (return nil) ...

    // Convert the manifest structure into a Rego object/value
	regoManifestValue, err := convertManifestToRegoValue(manifest)
	// ... handle error ...

	return ast.NewTerm(regoManifestValue), nil // Return the manifest data to the policy
}

// init registers functions like ec.oci.image_manifest with the Rego engine
func init() {
	rego.RegisterBuiltin1(/* ... ociImageManifest definition ... */)
	// ... register others like ec.oci.blob, ec.oci.descriptor ...
}

This shows that Rego policies can directly invoke functions like ec.oci.image_manifest. These functions, under the hood, use the same oci.Client to fetch data from the registry during policy evaluation. This allows for very flexible and powerful policies that can query registry information dynamically.

Conclusion

OCI Interaction is the critical component that allows ec-cli to communicate with container registries – the warehouses of the container world. It's responsible for:

  1. Finding the correct container image (box).
  2. Resolving tags to stable digests.
  3. Fetching the image's manifest (inventory) and configuration (setup instructions).
  4. Retrieving crucial security artifacts like signatures and attestations (labels and documents).
  5. Optionally extracting files from layers (contents).

While much of this happens automatically when you run validation commands, understanding this interaction is key to knowing where ec-cli gets the data it uses for evaluation.

This fetched data, especially signatures and attestations, is often related to cryptographic verification. In the next chapter, we'll delve deeper into how ec-cli specifically handles digital signatures using a popular standard.

Let's move on to Chapter 5: Sigstore Integration.


Generated by AI Codebase Knowledge Builder

Clone this wiki locally