docker/compose · error

invalid digest %s: %w

Error message

invalid digest %s: %w

What it means

Before hashing the downloaded bytes, GetBlob calls descriptor.Digest.Validate() to check that the digest string itself is well-formed (algorithm:hex with correct length/charset). A malformed digest — missing algorithm, wrong hex length for the algorithm, invalid characters, empty string — fails here. This is a descriptor-quality check distinct from content verification (error 97).

Source

Thrown at internal/oci/resolver.go:125

	}
	fetch, err := fetcher.Fetch(ctx, descriptor)
	if err != nil {
		return nil, fmt.Errorf("fetching blob %s: %w", descriptor.Digest, err)
	}
	defer func() { _ = fetch.Close() }()
	// bound the read by the declared size so a rogue registry can't cause
	// unbounded allocation; the extra byte detects oversized responses.
	content, err := io.ReadAll(io.LimitReader(fetch, descriptor.Size+1))
	if err != nil {
		return nil, fmt.Errorf("reading blob %s: %w", descriptor.Digest, err)
	}
	if int64(len(content)) != descriptor.Size {
		return nil, fmt.Errorf("blob %s size mismatch: expected %d bytes, got %d", descriptor.Digest, descriptor.Size, len(content))
	}
	// GetBlob bypasses containerd's content store, so integrity must be
	// checked here before callers write the bytes to disk.
	if err := descriptor.Digest.Validate(); err != nil {
		return nil, fmt.Errorf("invalid digest %s: %w", descriptor.Digest, err)
	}
	if actual := descriptor.Digest.Algorithm().FromBytes(content); actual != descriptor.Digest {
		return nil, fmt.Errorf("blob digest mismatch: expected %s, got %s", descriptor.Digest, actual)
	}
	return content, nil
}

func Copy(ctx context.Context, resolver remotes.Resolver, image reference.Named, named reference.Named) (spec.Descriptor, error) {
	src, desc, err := resolver.Resolve(ctx, image.String())
	if err != nil {
		return spec.Descriptor{}, err
	}
	if desc.Annotations == nil {
		desc.Annotations = make(map[string]string)
	}
	// set LabelDistributionSource so push will actually use a registry mount
	refspec := reference.TrimNamed(image).String()
	u, err := url.Parse("dummy://" + refspec)

View on GitHub (pinned to ddc4b044b6)

Solutions

  1. Use full, canonical digests: sha256:<64 lowercase hex chars>.
  2. Generate descriptors from real manifests (resolver.Resolve) rather than constructing them manually.
  3. Validate digests at ingestion time when reading them from user-supplied files.
  4. Check for truncation (e.g. 12-char short IDs are not valid full digests).

Example fix

// before
desc := spec.Descriptor{Digest: "sha256:deadbeef", Size: 123} // invalid hex length

// after
dg, _ := digest.Parse("sha256:<full-64-hex>")
desc := spec.Descriptor{Digest: dg, Size: 123}
Defensive patterns

Strategy: validation

Validate before calling

if err := descriptor.Digest.Validate(); err != nil {
    return fmt.Errorf("descriptor carries malformed digest %q: %w", descriptor.Digest, err)
}

Type guard

func isValidDigest(s string) bool {
    d, err := digest.Parse(s)
    return err == nil && d.Validate() == nil
}

Try / catch

if err := descriptor.Digest.Validate(); err != nil {
    return nil, fmt.Errorf("invalid digest %s: %w", descriptor.Digest, err)
    // reject the descriptor; do not attempt a fetch with it
}

Prevention

When it happens

Trigger: Passing a spec.Descriptor whose Digest string is like "sha256:xyz", "abc123" (no algorithm), "sha512:" (empty hex), or hex of the wrong length for the declared algorithm.

Common situations: Descriptors assembled by hand or parsed from custom YAML/JSON instead of from a registry manifest; truncated digest strings from config files; copy/paste typos in annotations or lockfiles.

Related errors


AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15). Data as JSON: /api/errors/2954c624f5b737c7. Report an issue: GitHub.