Repository navigation
b2_05_sigstore_integration_
In the previous chapter, we saw how ec-cli acts like a warehouse worker, fetching container images (boxes) and their related information from OCI registries (warehouses). But how do we know if the box hasn't been tampered with? How do we verify the authenticity seals and the accompanying paperwork?
This is where Sigstore Integration comes in. It's like the security guard for our software artifacts.
Imagine you receive a software package (our container image). You want to be sure it really came from the expected developer and that it hasn't been maliciously modified since it was created. You also might want to check if it comes with official documentation, like a list of ingredients (Software Bill of Materials - SBOM) or a record of how it was built (provenance).
Manually verifying all this is complex and involves cryptography. We need a standardized, automated way to:
- Check Digital Signatures: Verify that the software artifact was indeed "sealed" (signed) by a trusted entity (developer, build system).
- Check Attestations: Verify accompanying documents (attestations) that make claims about the software (e.g., its contents, build process, scan results).
- Increase Trust and Transparency: Ideally, use a public, verifiable system to record these signing events, making it harder for attackers to tamper undetected.
Our Goal: To understand how ec-cli uses the Sigstore project's tools to automatically verify the digital signatures and attestations associated with our container images.
Sigstore is a collection of tools and services designed to make signing and verifying software easier and more secure. Think of them as different specialists on the security team:
-
Digital Signatures (The Authenticity Seal):
- A cryptographic "seal" attached to a software artifact (like our container image).
- Proves who created the signature and that the artifact hasn't changed since it was signed.
-
Attestations (The Verified Paperwork):
- Statements or claims about a software artifact, which are themselves digitally signed.
- Examples:
- "This image was built using these steps." (SLSA Provenance)
- "This image contains these software packages." (SBOM)
- "This image passed a vulnerability scan."
-
ec-cliuses these attestations as input for policy checks.
-
Cosign (The Signing & Verification Tool):
- The primary tool
ec-cliuses to interact with Sigstore. - It handles the process of attaching signatures and attestations to container images (though
ec-climostly focuses on verification). - It also performs the verification checks: Does the signature match the image? Was it created by the expected identity?
- The primary tool
-
Fulcio (The ID Card Issuer):
- A free Certificate Authority (CA) specifically for code signing.
- It issues temporary code-signing certificates based on an OpenID Connect (OIDC) identity (like your GitHub or Google account).
- This enables "keyless" signing – you don't need to manage long-lived private keys; you sign using your existing identity via Fulcio.
-
Rekor (The Public Notary Log):
- A public, tamper-resistant transparency log.
- When software is signed (especially using the keyless method), details about the signing event (who signed what, when) are recorded in Rekor.
- During verification, Cosign (and thus
ec-cli) can check Rekor to ensure the signature was recorded publicly and hasn't been revoked. It's like checking a public notary's ledger.
-
Keyless vs. Keyed Signing (ID Card vs. Physical Key):
- Keyless (Modern): Uses short-lived certificates from Fulcio, linked to an OIDC identity, with the event logged in Rekor. Easier to manage, relies on the Sigstore infrastructure.
- Keyed (Traditional): Uses a long-lived pair of cryptographic keys (public and private). You protect the private key and share the public key. Requires careful key management.
ec-cli can verify artifacts signed using either method, depending on how the policy is configured.
You don't usually need to run cosign commands manually when using ec-cli for validation. ec-cli integrates Cosign's verification logic directly.
Here's how it works:
-
Policy Defines Requirements: Your Policy Definition & Evaluation rules specify what needs to be verified. The policy might say:
- "This image must be signed with this specific public key." (Keyed)
- "This image must be signed by someone with the identity
developer@example.comissued byhttps://oidc.example.com, and the signature must be in Rekor." (Keyless) - "This image must have a valid SLSA provenance attestation."
-
ec-cliFetches Artifacts: When you runec validate image,ec-cliuses its OCI Interaction capabilities to fetch the container image and any associated signatures or attestations stored alongside it in the registry. -
ec-cliCalls Cosign Libraries: Based on the policy requirements,ec-cliuses Cosign's underlying libraries to perform the verification:- It checks if the signature mathematically matches the image digest.
- If the policy specifies a public key, it checks if the signature was made by the corresponding private key.
- If the policy specifies an identity (for keyless), it checks the certificate attached to the signature:
- Was it issued by a trusted authority (like Fulcio)?
- Does the identity (subject, issuer) in the certificate match the policy?
- If the policy requires Rekor, it checks if the signature/attestation details exist in the Rekor log.
- It performs similar checks for attestations (verifying their signatures and identities).
-
Results Feed into Policy: The success or failure of these Sigstore verification checks becomes data available to the policy evaluation engine. Policy rules can then check things like:
PASS if signature_verified == trueFAIL if slsa_provenance_verified == falseFAIL if signature_identity_subject != "expected-builder@example.com"
Example Scenario:
Let's say our policy requires an image to be signed by the keyless identity ci-builder@my-org.com via the issuer https://github.com/login/oauth.
# Simplified Policy Snippet (conceptual)
publicKey: "" # Indicates keyless
identity:
issuer: https://github.com/login/oauth
subject: ci-builder@my-org.com
# rekorUrl: "https://rekor.sigstore.dev" # Often defaultWhen you run ec validate image --image my-image@sha256:abc... --policy my-policy.yaml:
-
ec-clireads the policy, sees it's keyless, and notes the required issuer/subject. - It fetches
my-image@sha256:abc...and its associated signatures/attestations from the OCI registry. - For each signature, it uses Cosign libraries to:
- Verify the signature against the image digest.
- Extract the signing certificate.
- Check the certificate against the public Fulcio root certificate.
- Check if the Issuer in the certificate matches
https://github.com/login/oauth. - Check if the Subject in the certificate matches
ci-builder@my-org.com. - Query Rekor (using the default URL) to confirm the signature is logged.
- If all checks pass for at least one signature, the verification part succeeds. This success/failure information is then used by the Rego policy rules.
High-Level Steps:
-
Parse Policy:
ec-clireads the policy configuration to understand the verification requirements (public key, identity, Rekor URL, ignore Rekor?). -
Fetch Data: It uses OCI Interaction to fetch the image manifest, config, and any
.sig(signature) or.att(attestation) artifacts associated with the image digest in the registry. -
Prepare Cosign Options: Based on the policy, it configures the
cosign.CheckOptsstructure, setting parameters like the public key verifier or the expected identities and Rekor client. -
Invoke Cosign Verification: It calls the appropriate Cosign library functions (e.g.,
VerifyImageSignatures,VerifyImageAttestations) passing the image reference and the preparedCheckOpts. - Cosign Performs Checks: The Cosign library interacts with the registry, potentially Fulcio (to get root certificates), and Rekor (to verify transparency log entries) as needed.
- Return Results: Cosign returns the verified signatures/attestations or an error if verification failed.
-
Process Results:
ec-climakes these verified artifacts (or the failure status) available for policy evaluation.
Simplified Sequence Diagram:
sequenceDiagram
participant User
participant CLI as ec validate image
participant Policy as Policy Config
participant OCIClient as OCI Client
participant CosignLib as Cosign Libs
participant SigstoreInfra as Fulcio/Rekor
User->>CLI: Run validate command
CLI->>Policy: Read verification requirements (key/identity)
Policy-->>CLI: Return requirements
CLI->>OCIClient: Fetch image signatures/attestations
OCIClient-->>CLI: Return signature/attestation data
CLI->>CosignLib: Prepare CheckOpts based on Policy
CLI->>CosignLib: Verify(imageRef, CheckOpts, sig/att data)
CosignLib->>SigstoreInfra: Check Cert Chain (Fulcio Roots)?
SigstoreInfra-->>CosignLib: Cert status
CosignLib->>SigstoreInfra: Check Rekor Log?
SigstoreInfra-->>CosignLib: Log status
CosignLib-->>CLI: Verification Result (Success/Fail, Verified Artifacts)
CLI-->>User: (after policy eval) Show final report
Let's see where this integration happens in the code.
1. Configuring Cosign Checks Based on Policy (internal/policy/policy.go)
The NewPolicy function reads the policy configuration and the checkOpts helper function prepares the cosign.CheckOpts structure needed by the Cosign library.
// Simplified concept from internal/policy/policy.go
// NewPolicy creates a Policy object based on references and options
func NewPolicy(ctx context.Context, opts Options) (Policy, error) {
p := policy{ /* ... */ }
// Load policy spec from Kubernetes or file/string
if err := p.loadPolicy(ctx, opts.PolicyRef); err != nil {
return nil, err
}
// Override PublicKey, RekorURL etc. from command-line if provided
if opts.PublicKey != "" { p.PublicKey = opts.PublicKey }
// ... similar overrides for RekorURL, Identity ...
// Determine if keyless verification is used
if p.PublicKey == "" {
// Set identity from policy spec or command-line opts
p.identity = determineIdentity(opts, p.EnterpriseContractPolicySpec)
if err := validateIdentity(p.identity); err != nil { // Ensure required fields are set
return nil, err
}
}
// Create the cosign CheckOpts based on the policy settings
if cosignOpts, err := checkOpts(ctx, &p); err != nil {
return nil, err
} else {
p.checkOpts = cosignOpts // Store the configured CheckOpts
}
return &p, nil
}
// checkOpts prepares the cosign.CheckOpts struct
func checkOpts(ctx context.Context, p *policy) (*cosign.CheckOpts, error) {
opts := cosign.CheckOpts{}
var err error
if p.PublicKey != "" { // Keyed verification
log.Debug("Using long-lived key workflow")
// Load public key verifier from file, KMS, etc.
opts.SigVerifier, err = signatureVerifier(ctx, p.PublicKey)
// ... handle error ...
} else { // Keyless verification
log.Debug("Using keyless workflow")
opts.Identities = []cosign.Identity{p.identity} // Set expected identities
// Fetch trusted root certs (e.g., from Fulcio via TUF)
opts.RootCerts, err = fulcio.GetRoots()
// ... handle error ...
opts.IntermediateCerts, err = fulcio.GetIntermediates()
// ... handle error ...
// Fetch Certificate Transparency Log keys
opts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
// ... handle error ...
}
// Configure Rekor client and keys if needed
opts.IgnoreTlog = p.ignoreRekor
if !opts.IgnoreTlog {
// Get Rekor client based on p.RekorUrl
opts.RekorClient, err = rekor.NewClient(p.RekorUrl)
// ... handle error ...
// Get trusted Rekor public keys (via TUF)
opts.RekorPubKeys, err = cosign.GetRekorPubs(ctx)
// ... handle error ...
}
return &opts, nil
}This shows how the policy configuration (like PublicKey, Identity, RekorUrl) directly translates into settings within the cosign.CheckOpts structure, which tells the Cosign library exactly how to perform the verification.
2. Invoking Verification (internal/evaluation_target/application_snapshot_image/application_snapshot_image.go)
The ApplicationSnapshotImage type calls methods on the OCI Interaction client, which uses the Cosign library internally.
// Simplified concept from internal/evaluation_target/application_snapshot_image/application_snapshot_image.go
// ValidateImageSignature verifies image signatures using Cosign via the OCI client
func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) error {
opts := a.checkOpts // Get the pre-configured CheckOpts from the policy
opts.ClaimVerifier = cosign.SimpleClaimVerifier // Specific verifier for image signatures
// Use the OCI Client (which wraps Cosign calls)
signatures, _, err := oci.NewClient(ctx).VerifyImageSignatures(a.reference, &opts)
if err != nil {
return err // Verification failed
}
// Store successfully verified signatures
for _, sig := range signatures {
// Convert Cosign signature to internal format
es, err := signature.NewEntitySignature(sig)
// ... handle conversion error ...
a.signatures = append(a.signatures, es)
}
return nil // Verification succeeded
}
// ValidateAttestationSignature verifies attestation signatures using Cosign via the OCI client
func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Context) error {
opts := a.checkOpts // Get the pre-configured CheckOpts from the policy
opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier // Specific verifier for attestations
// Use the OCI Client (which wraps Cosign calls)
layers, _, err := oci.NewClient(ctx).VerifyImageAttestations(a.reference, &opts)
if err != nil {
return err // Verification failed
}
// Store successfully verified attestations and their signatures
for _, sigLayer := range layers {
// Parse the layer payload as an attestation
att, err := attestation.ProvenanceFromSignature(sigLayer)
// ... handle parsing error ...
a.attestations = append(a.attestations, att)
}
return nil // Verification succeeded
}These methods show ec-cli calling the verification functions (provided by the OCI client abstraction, which uses Cosign libraries). It passes the image reference and the CheckOpts configured from the policy. The verified signatures and attestations are stored for later use by the policy engine.
3. OCI Client Interface (internal/utils/oci/client.go)
The OCI client interface clearly defines the methods used for Sigstore verification.
// Simplified concept from internal/utils/oci/client.go
package oci
type Client interface {
// ... other methods like Head, ResolveDigest, Image ...
// Verifies signatures associated with the image reference using Cosign options
VerifyImageSignatures(ref name.Reference, opts *cosign.CheckOpts) ([]oci.Signature, bool, error)
// Verifies attestations associated with the image reference using Cosign options
VerifyImageAttestations(ref name.Reference, opts *cosign.CheckOpts) ([]oci.Signature, bool, error)
}
// The actual implementation of these methods uses Cosign libraries
// like cosign.VerifyImageSignatures(...) and cosign.VerifyImageAttestations(...)This confirms that the OCI client abstraction is the bridge between ec-cli's core logic and the underlying Cosign verification functions.
4. Rego Built-in Functions (internal/rego/sigstore/sigstore.go)
Policies can also directly trigger Sigstore verification during evaluation using custom Rego functions.
// Simplified concept from internal/rego/sigstore/sigstore.go
// Example Rego function definition
func registerSigstoreVerifyAttestation() {
decl := rego.Function{
Name: "ec.sigstore.verify_attestation",
Description: "Use sigstore to verify the attestation of an image.",
Decl: types.NewFunction(
types.Args(ociImageReferenceParameter, sigstoreOptsParameter), // Input: image ref, options
result, // Output: verification result (success, errors, attestations)
),
// ... flags like Memoize, Nondeterministic ...
}
rego.RegisterBuiltin2(&decl, sigstoreVerifyAttestation) // Register the function
}
// The implementation function called by Rego
func sigstoreVerifyAttestation(bctx rego.BuiltinContext, refTerm *ast.Term, optsTerm *ast.Term) (*ast.Term, error) {
// 1. Parse image reference 'refTerm' and options 'optsTerm'
ref := parseImageRef(refTerm)
checkOpts := parseCheckOpts(bctx.Context, optsTerm) // Parse opts into cosign.CheckOpts
// 2. Call OCI client to perform verification (using Cosign)
attestations, _, err := ecoci.NewClient(bctx.Context).VerifyImageAttestations(ref, checkOpts)
// 3. Format the results (success/fail, errors, verified attestations) into a Rego term
resultTerm := formatAttestationResult(attestations, err)
// 4. Return the result term to the Rego policy
return resultTerm, nil // Errors are handled *within* the resultTerm for Rego
}This shows that even Rego policies can dynamically invoke Sigstore verification for specific images using defined options, allowing for more complex policy logic. The built-in function essentially wraps the same Cosign verification process we saw earlier.
Sigstore Integration is how ec-cli acts as a security guard, verifying the authenticity and integrity of your software artifacts. By leveraging Cosign, Fulcio, and Rekor, ec-cli can automatically:
- Check digital signatures against public keys or keyless identities.
- Verify signed attestations (like SLSA provenance or SBOMs).
- Optionally ensure signings are recorded in the Rekor transparency log.
This verification is driven by your Policy Definition & Evaluation rules and happens seamlessly when you validate an Application Snapshot. It provides strong cryptographic assurance that the software you are evaluating meets your security requirements.
Now that we understand how ec-cli validates artifacts, how can we integrate these checks into automated pipelines?
Let's move on to Chapter 6: Tekton Task Integration to see how ec-cli fits into CI/CD workflows.
Generated by AI Codebase Knowledge Builder