2024-10-17 13:40:17 -05:00
|
|
|
/*
|
2024-10-18 09:25:31 -05:00
|
|
|
Copyright Docker attest authors
|
2024-10-17 13:40:17 -05:00
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
2024-10-18 09:25:31 -05:00
|
|
|
|
2024-04-29 12:52:39 -05:00
|
|
|
package attest
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2024-08-23 09:33:30 +01:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2024-07-12 17:09:41 +01:00
|
|
|
"strings"
|
2024-05-22 14:49:23 +01:00
|
|
|
"time"
|
2024-04-29 12:52:39 -05:00
|
|
|
|
2024-07-12 17:09:41 +01:00
|
|
|
"github.com/distribution/reference"
|
2024-09-02 16:17:50 +01:00
|
|
|
"github.com/docker/attest/attestation"
|
2024-09-18 21:11:55 +01:00
|
|
|
"github.com/docker/attest/mapping"
|
2024-09-02 16:17:50 +01:00
|
|
|
"github.com/docker/attest/oci"
|
|
|
|
|
"github.com/docker/attest/policy"
|
|
|
|
|
"github.com/docker/attest/tuf"
|
2024-10-16 12:01:31 -05:00
|
|
|
"github.com/docker/attest/version"
|
2024-05-22 14:49:23 +01:00
|
|
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
2024-04-29 12:52:39 -05:00
|
|
|
)
|
|
|
|
|
|
2024-09-18 13:34:10 +01:00
|
|
|
type ImageVerifier struct {
|
|
|
|
|
opts *policy.Options
|
|
|
|
|
tufClient tuf.Downloader
|
|
|
|
|
attestationVerifier attestation.Verifier
|
2024-10-16 12:01:31 -05:00
|
|
|
versionFetcher version.Fetcher
|
2024-08-28 09:53:52 +01:00
|
|
|
}
|
|
|
|
|
|
2024-09-18 13:34:10 +01:00
|
|
|
func NewImageVerifier(ctx context.Context, opts *policy.Options) (*ImageVerifier, error) {
|
2024-08-28 09:53:52 +01:00
|
|
|
err := populateDefaultOptions(opts)
|
2024-08-23 09:33:30 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2024-06-21 22:12:42 +01:00
|
|
|
}
|
2024-08-28 09:53:52 +01:00
|
|
|
var tufClient tuf.Downloader
|
|
|
|
|
if !opts.DisableTUF {
|
2024-09-09 14:22:17 +01:00
|
|
|
tufClient, err = tuf.NewClient(ctx, opts.TUFClientOptions)
|
2024-08-23 09:33:30 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create TUF client: %w", err)
|
|
|
|
|
}
|
2024-06-21 22:12:42 +01:00
|
|
|
}
|
2024-09-18 13:34:10 +01:00
|
|
|
attestationVerifier := opts.AttestationVerifier
|
|
|
|
|
if attestationVerifier == nil {
|
|
|
|
|
attestationVerifier, err = attestation.NewVerfier(attestation.WithTUFDownloader(tufClient))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create attestation verifier: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return &ImageVerifier{
|
|
|
|
|
opts: opts,
|
|
|
|
|
tufClient: tufClient,
|
|
|
|
|
attestationVerifier: attestationVerifier,
|
2024-10-16 12:01:31 -05:00
|
|
|
versionFetcher: version.NewGoVersionFetcher(),
|
2024-08-28 09:53:52 +01:00
|
|
|
}, nil
|
|
|
|
|
}
|
2024-08-23 09:33:30 +01:00
|
|
|
|
2024-09-18 13:34:10 +01:00
|
|
|
func (verifier *ImageVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) {
|
2024-08-28 09:53:52 +01:00
|
|
|
// so that we can resolve mapping from the image name earlier
|
|
|
|
|
detailsResolver, err := policy.CreateImageDetailsResolver(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
|
|
|
|
|
}
|
2024-08-28 11:27:00 +01:00
|
|
|
imageName, err := detailsResolver.ImageName(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to resolve image name: %w", err)
|
|
|
|
|
}
|
2024-08-29 17:43:45 +01:00
|
|
|
policyResolver := policy.NewResolver(verifier.tufClient, verifier.opts)
|
2024-09-18 21:11:55 +01:00
|
|
|
|
|
|
|
|
platform, err := detailsResolver.ImagePlatform(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to get image platform: %w", err)
|
|
|
|
|
}
|
|
|
|
|
resolvedPolicy, err := policyResolver.ResolvePolicy(ctx, imageName, platform)
|
2024-05-22 14:49:23 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-29 17:43:45 +01:00
|
|
|
if resolvedPolicy == nil {
|
2024-05-22 14:49:23 +01:00
|
|
|
return &VerificationResult{
|
|
|
|
|
Outcome: OutcomeNoPolicy,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
2024-06-21 11:29:16 +01:00
|
|
|
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
2024-08-29 17:43:45 +01:00
|
|
|
if verifier.opts.ReferrersRepo != "" {
|
2024-09-18 21:11:55 +01:00
|
|
|
resolvedPolicy.Mapping.Attestations = &mapping.AttestationConfig{
|
2024-08-29 17:43:45 +01:00
|
|
|
Repo: verifier.opts.ReferrersRepo,
|
2024-09-18 21:11:55 +01:00
|
|
|
Style: mapping.AttestationStyleReferrers,
|
2024-06-21 22:12:42 +01:00
|
|
|
}
|
2024-09-18 21:11:55 +01:00
|
|
|
} else if verifier.opts.AttestationStyle == mapping.AttestationStyleAttached {
|
|
|
|
|
resolvedPolicy.Mapping.Attestations = &mapping.AttestationConfig{
|
2024-08-29 17:43:45 +01:00
|
|
|
Repo: verifier.opts.ReferrersRepo,
|
2024-09-18 21:11:55 +01:00
|
|
|
Style: mapping.AttestationStyleAttached,
|
2024-06-21 11:29:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached)
|
2024-08-29 17:43:45 +01:00
|
|
|
resolver, err := policy.CreateAttestationResolver(detailsResolver, resolvedPolicy.Mapping)
|
2024-06-21 11:29:16 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
|
|
|
|
}
|
2024-09-18 13:34:10 +01:00
|
|
|
evaluator := policy.NewRegoEvaluator(verifier.opts.Debug, verifier.attestationVerifier)
|
2024-10-16 12:01:31 -05:00
|
|
|
result, err = verifier.verifyAttestations(ctx, resolver, evaluator, resolvedPolicy)
|
2024-05-22 14:49:23 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-28 09:53:52 +01:00
|
|
|
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) {
|
2024-09-18 13:34:10 +01:00
|
|
|
verifier, err := NewImageVerifier(ctx, opts)
|
2024-08-28 09:53:52 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return verifier.Verify(ctx, src)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 09:33:30 +01:00
|
|
|
func populateDefaultOptions(opts *policy.Options) (err error) {
|
2024-08-28 09:53:52 +01:00
|
|
|
if opts.LocalPolicyDir == "" && opts.DisableTUF {
|
|
|
|
|
return fmt.Errorf("local policy dir must be set if not using TUF")
|
|
|
|
|
}
|
2024-08-23 09:33:30 +01:00
|
|
|
if opts.LocalTargetsDir == "" {
|
|
|
|
|
opts.LocalTargetsDir, err = defaultLocalTargetsDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-28 09:53:52 +01:00
|
|
|
if opts.DisableTUF && opts.TUFClientOptions != nil {
|
|
|
|
|
return fmt.Errorf("TUF client options set but TUF disabled")
|
|
|
|
|
} else if opts.TUFClientOptions == nil && !opts.DisableTUF {
|
2024-08-23 09:33:30 +01:00
|
|
|
opts.TUFClientOptions = tuf.NewDockerDefaultClientOptions(opts.LocalTargetsDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.AttestationStyle == "" {
|
2024-09-18 21:11:55 +01:00
|
|
|
opts.AttestationStyle = mapping.AttestationStyleReferrers
|
2024-08-23 09:33:30 +01:00
|
|
|
}
|
2024-09-18 21:11:55 +01:00
|
|
|
if opts.ReferrersRepo != "" && opts.AttestationStyle != mapping.AttestationStyleReferrers {
|
2024-08-23 09:33:30 +01:00
|
|
|
return fmt.Errorf("referrers repo specified but attestation source not set to referrers")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func defaultLocalTargetsDir() (string, error) {
|
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(homeDir, ".docker", "tuf"), nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-16 12:01:31 -05:00
|
|
|
func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result, versionFetcher version.Fetcher) (*VerificationResult, error) {
|
2024-05-22 14:49:23 +01:00
|
|
|
dgst, err := oci.SplitDigest(input.Digest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to split digest: %w", err)
|
|
|
|
|
}
|
|
|
|
|
subject := intoto.Subject{
|
2024-08-01 15:35:15 +01:00
|
|
|
Name: input.PURL,
|
2024-06-17 17:24:29 +01:00
|
|
|
Digest: dgst,
|
2024-05-22 14:49:23 +01:00
|
|
|
}
|
2024-08-01 15:35:15 +01:00
|
|
|
resourceURI, err := attestation.ToVSAResourceURI(subject)
|
2024-05-22 14:49:23 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create resource uri: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var outcome Outcome
|
|
|
|
|
if result.Success {
|
|
|
|
|
outcome = OutcomeSuccess
|
|
|
|
|
} else {
|
|
|
|
|
outcome = OutcomeFailure
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outcomeStr, err := outcome.StringForVSA()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-14 12:32:51 -05:00
|
|
|
vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI, DownloadLocation: p.URI, Digest: p.Digest}
|
2024-10-16 12:01:31 -05:00
|
|
|
attestVersion, err := attestation.GetVerifierVersion(versionFetcher)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to get verifier version: %w", err)
|
|
|
|
|
}
|
2024-08-14 09:16:24 -05:00
|
|
|
|
2024-05-22 14:49:23 +01:00
|
|
|
return &VerificationResult{
|
|
|
|
|
Policy: p,
|
|
|
|
|
Outcome: outcome,
|
|
|
|
|
Violations: result.Violations,
|
2024-06-17 17:28:22 +01:00
|
|
|
Input: input,
|
2024-05-22 14:49:23 +01:00
|
|
|
VSA: &intoto.Statement{
|
|
|
|
|
StatementHeader: intoto.StatementHeader{
|
|
|
|
|
PredicateType: attestation.VSAPredicateType,
|
|
|
|
|
Type: intoto.StatementInTotoV01,
|
|
|
|
|
Subject: result.Summary.Subjects,
|
|
|
|
|
},
|
|
|
|
|
Predicate: attestation.VSAPredicate{
|
|
|
|
|
Verifier: attestation.VSAVerifier{
|
2024-10-16 12:01:31 -05:00
|
|
|
ID: result.Summary.Verifier,
|
|
|
|
|
Version: attestVersion,
|
2024-05-22 14:49:23 +01:00
|
|
|
},
|
|
|
|
|
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
2024-08-01 15:35:15 +01:00
|
|
|
ResourceURI: resourceURI,
|
2024-08-14 09:16:24 -05:00
|
|
|
Policy: vsaPolicy,
|
2024-05-22 14:49:23 +01:00
|
|
|
VerificationResult: outcomeStr,
|
|
|
|
|
VerifiedLevels: result.Summary.SLSALevels,
|
2024-10-07 13:36:30 -05:00
|
|
|
InputAttestations: result.Summary.Inputs,
|
2024-05-22 14:49:23 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-16 12:01:31 -05:00
|
|
|
func (verifier *ImageVerifier) verifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy) (*VerificationResult, error) {
|
2024-07-16 10:05:17 +01:00
|
|
|
desc, err := resolver.ImageDescriptor(ctx)
|
2024-04-29 12:52:39 -05:00
|
|
|
if err != nil {
|
2024-07-16 10:05:17 +01:00
|
|
|
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
2024-04-29 12:52:39 -05:00
|
|
|
}
|
2024-07-16 10:05:17 +01:00
|
|
|
digest := desc.Digest.String()
|
2024-04-29 12:52:39 -05:00
|
|
|
name, err := resolver.ImageName(ctx)
|
|
|
|
|
if err != nil {
|
2024-05-22 14:49:23 +01:00
|
|
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
2024-04-29 12:52:39 -05:00
|
|
|
}
|
2024-06-21 11:29:16 +01:00
|
|
|
platform, err := resolver.ImagePlatform(ctx)
|
2024-05-30 17:38:58 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-07-12 17:09:41 +01:00
|
|
|
|
2024-08-29 17:43:45 +01:00
|
|
|
if resolvedPolicy.ResolvedName != "" {
|
2024-07-12 17:09:41 +01:00
|
|
|
// this means the name we have is not the one we want to use for policy evaluation
|
|
|
|
|
// so we need to replace it with the one we resolved during policy resolution.
|
|
|
|
|
// this can happen if the name is an alias for another image, e.g. if it is a mirror
|
|
|
|
|
ref, err := reference.ParseNormalizedNamed(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
oldName := ref.Name()
|
2024-08-29 17:43:45 +01:00
|
|
|
name = strings.Replace(name, oldName, resolvedPolicy.ResolvedName, 1)
|
2024-09-05 14:08:55 +01:00
|
|
|
resolver = WithImageName(resolver, resolvedPolicy.ResolvedName)
|
2024-07-12 17:09:41 +01:00
|
|
|
}
|
|
|
|
|
|
2024-08-21 18:01:11 +01:00
|
|
|
ref, err := reference.ParseNormalizedNamed(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to parse ref %q: %w", ref, err)
|
|
|
|
|
}
|
|
|
|
|
purl, canonical, err := oci.RefToPURL(ref, platform)
|
2024-04-29 12:52:39 -05:00
|
|
|
if err != nil {
|
2024-05-22 14:49:23 +01:00
|
|
|
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
2024-04-29 12:52:39 -05:00
|
|
|
}
|
2024-08-21 18:01:11 +01:00
|
|
|
var tag string
|
|
|
|
|
if !canonical {
|
|
|
|
|
// unlike the function name indicates, this adds latest if no tag is present
|
|
|
|
|
ref = reference.TagNameOnly(ref)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tagged, ok := ref.(reference.Tagged); ok {
|
|
|
|
|
tag = tagged.Tag()
|
|
|
|
|
}
|
2024-08-01 15:35:15 +01:00
|
|
|
input := &policy.Input{
|
2024-08-21 18:01:11 +01:00
|
|
|
Digest: digest,
|
|
|
|
|
PURL: purl,
|
|
|
|
|
Platform: platform.String(),
|
|
|
|
|
Domain: reference.Domain(ref),
|
|
|
|
|
NormalizedName: reference.Path(ref),
|
|
|
|
|
FamiliarName: reference.FamiliarName(ref),
|
2024-10-16 12:01:31 -05:00
|
|
|
Parameters: verifier.opts.Parameters,
|
2024-08-21 18:01:11 +01:00
|
|
|
}
|
|
|
|
|
// rego has null strings
|
|
|
|
|
if tag != "" {
|
|
|
|
|
input.Tag = tag
|
2024-04-29 12:52:39 -05:00
|
|
|
}
|
2024-08-29 17:43:45 +01:00
|
|
|
result, err := evaluator.Evaluate(ctx, resolver, resolvedPolicy, input)
|
2024-04-29 12:52:39 -05:00
|
|
|
if err != nil {
|
2024-05-22 14:49:23 +01:00
|
|
|
return nil, fmt.Errorf("policy evaluation failed: %w", err)
|
2024-04-29 12:52:39 -05:00
|
|
|
}
|
2024-10-16 12:01:31 -05:00
|
|
|
verificationResult, err := toVerificationResult(resolvedPolicy, input, result, verifier.versionFetcher)
|
2024-07-16 10:05:17 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
|
|
|
|
|
}
|
|
|
|
|
verificationResult.SubjectDescriptor = desc
|
|
|
|
|
return verificationResult, nil
|
|
|
|
|
}
|
2024-09-05 14:08:55 +01:00
|
|
|
|
|
|
|
|
// WithImageName returns a new resolver that will return the given image name as this can be updated by the mapping file.
|
|
|
|
|
func WithImageName(resolver attestation.Resolver, s string) attestation.Resolver {
|
|
|
|
|
return &wrappedResolver{
|
|
|
|
|
Resolver: resolver,
|
|
|
|
|
imageName: s,
|
|
|
|
|
}
|
|
|
|
|
}
|