lib/dockerregistry/manifests.go (150 lines of code) (raw):

// Copyright (c) 2016-2019 Uber Technologies, Inc. // // 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. package dockerregistry import ( "fmt" "strings" "time" "github.com/uber/kraken/utils/closers" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/uber/kraken/core" "github.com/uber/kraken/lib/dockerregistry/transfer" "github.com/uber/kraken/lib/store" "github.com/uber/kraken/utils/log" ) type SignatureVerificationDecision int const ( DecisionSkip SignatureVerificationDecision = iota DecisionDeny DecisionAllow ) type manifests struct { transferer transfer.ImageTransferer verification func(repo string, digest core.Digest, blob store.FileReader) (SignatureVerificationDecision, error) } func newManifests( transferer transfer.ImageTransferer, verification func(repo string, digest core.Digest, blob store.FileReader) (SignatureVerificationDecision, error), ) *manifests { return &manifests{ transferer: transferer, verification: verification, } } // getDigest resolves and downloads a manifest blob (by tag or digest) and // returns its digest as bytes. // // Behavior // 1. Extracts the repository from the provided registry path. // 2. If subtype is tags, resolves the tag to a digest using the transferer; // if subtype is revisions, parses the digest directly from the path. // 3. Downloads the manifest blob via the transferer using (repo, digest). // 4. Opportunistically invokes verify to run signature/image checks and // record logs. Verification result is not enforced here. // 5. Returns the digest in ASCII string form as a byte slice. // // Notes // - This is the single place where a manifest is actually fetched via the // transferer (torrent/origin), since it has the namespace (repo) context. // - Callers typically invoke getDigest first to ensure the blob is local, // then call Stat/Reader which assume the blob is already on disk. func (t *manifests) getDigest(path string, subtype PathSubType) ([]byte, error) { repo, err := GetRepo(path) if err != nil { return nil, fmt.Errorf("get repo: %s", err) } var digest core.Digest switch subtype { case _tags: tag, _, err := GetManifestTag(path) if err != nil { return nil, fmt.Errorf("get manifest tag: %s", err) } digest, err = t.transferer.GetTag(fmt.Sprintf("%s:%s", repo, tag)) if err != nil { return nil, fmt.Errorf("transferer get tag: %w", err) } case _revisions: var err error digest, err = GetManifestDigest(path) if err != nil { return nil, fmt.Errorf("get manifest digest: %s", err) } default: return nil, &InvalidRequestError{path} } blob, err := t.transferer.Download(repo, digest) if err != nil { return nil, fmt.Errorf("transferer download: %w", err) } defer closers.Close(blob) // Signature verification is currently not enforced: errors from t.verify are ignored. // This is intentional because verification enforcement is planned for a future release. // Risks: manifests may be accepted without verification, which could allow untrusted content. // TODO: Remove error ignoring and enforce verification once the feature is activated. _, _ = t.verify(path, repo, digest, blob) //nolint:errcheck return []byte(digest.String()), nil } // verify runs signature/image verification for a downloaded manifest blob and // logs around the decision. // // Returns // - (true, nil) when verification is allowed or intentionally skipped. // - (false, nil) when verification explicitly denies. // - (false, err) on verification errors or unknown decisions. // // Logging // - Error on verification error (includes repo/digest). // - Warn on deny (includes original path). // - Debug on skip. func (t *manifests) verify( path string, repo string, digest core.Digest, blob store.FileReader, ) (bool, error) { decision, err := t.verification(repo, digest, blob) if err != nil { log.With("repo", repo, "digest", digest).Errorf("Error while performing image validation %s", err) return false, err } switch decision { case DecisionAllow: return true, nil case DecisionDeny: log.With("repo", repo, "digest", digest).Warnf("Verification failed %s", path) return false, nil case DecisionSkip: log.With("repo", repo, "digest", digest).Debugf("Verification skipped for %s", path) return true, nil default: return false, fmt.Errorf("unknown verification decision: %d", decision) } } func (t *manifests) putContent(path string, subtype PathSubType) error { switch subtype { case _tags: repo, err := GetRepo(path) if err != nil { return fmt.Errorf("get repo: %s", err) } tag, isCurrent, err := GetManifestTag(path) if err != nil { return fmt.Errorf("get manifest tag: %s", err) } if isCurrent { return nil } digest, err := GetManifestDigest(path) if err != nil { return fmt.Errorf("get manifest digest: %s", err) } if err := t.transferer.PutTag(fmt.Sprintf("%s:%s", repo, tag), digest); err != nil { return fmt.Errorf("post tag: %w", err) } return nil } // Intentional no-op. return nil } func (t *manifests) stat(path string) (storagedriver.FileInfo, error) { repo, err := GetRepo(path) if err != nil { return nil, fmt.Errorf("get repo: %s", err) } tag, _, err := GetManifestTag(path) if err != nil { return nil, fmt.Errorf("get manifest tag: %s", err) } if _, err := t.transferer.GetTag(fmt.Sprintf("%s:%s", repo, tag)); err != nil { return nil, fmt.Errorf("get tag: %w", err) } return storagedriver.FileInfoInternal{ FileInfoFields: storagedriver.FileInfoFields{ Path: path, Size: 64, ModTime: time.Now(), IsDir: false, }, }, nil } func (t *manifests) list(path string) ([]string, error) { prefix := path[len(_repositoryRoot):] tags, err := t.transferer.ListTags(prefix) if err != nil { return nil, err } for i, tag := range tags { // Strip repo prefix. parts := strings.Split(tags[i], ":") if len(parts) != 2 { log.With("tag", tag).Warn("Repo list skipping tag, expected repo:tag format") continue } tags[i] = parts[1] } return tags, nil }