Skip to content

Follow OCI distribution spec for artifactType and annotations#2269

Merged
Subserial merged 2 commits into
google:mainfrom
malt3:referrers-api-use-correct-artifact-type
Apr 20, 2026
Merged

Follow OCI distribution spec for artifactType and annotations#2269
Subserial merged 2 commits into
google:mainfrom
malt3:referrers-api-use-correct-artifact-type

Conversation

@malt3

@malt3 malt3 commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Per the OCI distribution spec (distribution-spec#395), the artifactType
on a descriptor should be set to the manifest's artifactType if present,
falling back to config.mediaType unconditionally (not only for
non-standard config types as was done previously). Additionally,
annotations from the manifest are now copied to the descriptor.

Current wording from the spec:

Pushing Manifests with Subject

When processing a request for an image manifest with the subject field,
a registry implementation that supports the referrers API MUST respond
with the response header OCI-Subject: to indicate to
the client that the registry processed the request's subject.

When pushing a manifest with the subject field and the OCI-Subject header was not set, the client MUST:

  1. Pull the current referrers list using the referrers tag schema.
  2. If that pull returns a manifest other than the expected image index, the client SHOULD report a failure and skip the remaining steps.
  3. If the tag returns a 404, the client MUST begin with an empty image index.
  4. Verify the descriptor for the manifest is not already in the referrers list (duplicate entries SHOULD NOT be created).
  5. Append a descriptor for the pushed manifest to the manifests in the referrers list. The value of the artifactType MUST be set to the artifactType value in the pushed manifest, if present. If the artifactType is empty or missing in a pushed image manifest, the value of artifactType MUST be set to the config descriptor mediaType value. All annotations from the pushed manifest MUST be copied to this descriptor.
  6. Push the updated referrers list using the same referrers tag schema. The client MAY use conditional HTTP requests to prevent overwriting a referrers list that has changed since it was first pulled.

With this change, we correctly follow step 5.

Changes:

  • Add ArtifactType field to v1.Manifest and v1.IndexManifest structs
  • Update fetchManifest to prefer manifest artifactType, fall back to
    config.mediaType, and copy manifest annotations to the descriptor
  • Apply the same artifactType logic in remote/index.go, remote/write.go,
    and partial/with.go

malt3 added 2 commits April 20, 2026 11:27
Per the OCI distribution spec (distribution-spec#395), the artifactType
on a descriptor should be set to the manifest's artifactType if present,
falling back to config.mediaType unconditionally (not only for
non-standard config types as was done previously). Additionally,
annotations from the manifest are now copied to the descriptor.

Current wording from the spec:

> Pushing Manifests with Subject
>
> When processing a request for an image manifest with the subject field,
> a registry implementation that supports the referrers API MUST respond
> with the response header OCI-Subject: <subject digest> to indicate to
> the client that the registry processed the request's subject.
>
> When pushing a manifest with the subject field and the OCI-Subject header was not set, the client MUST:
> 1. Pull the current referrers list using the referrers tag schema.
> 2. If that pull returns a manifest other than the expected image index, the client SHOULD report a failure and skip the remaining steps.
> 3. If the tag returns a 404, the client MUST begin with an empty image index.
> 4. Verify the descriptor for the manifest is not already in the referrers list (duplicate entries SHOULD NOT be created).
> 5. Append a descriptor for the pushed manifest to the manifests in the referrers list. The value of the artifactType MUST be set to the artifactType value in the pushed manifest, if present. If the artifactType is empty or missing in a pushed image manifest, the value of artifactType MUST be set to the config descriptor mediaType value. All annotations from the pushed manifest MUST be copied to this descriptor.
> 6. Push the updated referrers list using the same referrers tag schema. The client MAY use conditional HTTP requests to prevent overwriting a referrers list that has changed since it was first pulled.

With this change, we correctly follow step 5.

Changes:
- Add ArtifactType field to v1.Manifest and v1.IndexManifest structs
- Update fetchManifest to prefer manifest artifactType, fall back to
  config.mediaType, and copy manifest annotations to the descriptor
- Apply the same artifactType logic in remote/index.go, remote/write.go,
  and partial/with.go
Test the OCI distribution spec behavior for artifactType resolution
and annotation copying:

- TestGet_ArtifactTypeAndAnnotations: end-to-end test via Get() covering
  fallback to config.mediaType, explicit artifactType precedence, and
  annotation copying to the descriptor
- TestGet_NonManifestMediaType: unparseable manifests produce no
  artifactType or annotations
- TestArtifactType_Fallback: partial.ArtifactType falls back to
  config.mediaType for standard, OCI, and custom config types
- TestDescriptor_ArtifactType_Fallback: partial.Descriptor does the same
- TestArtifactType_ExplicitArtifactType: explicit manifest artifactType
  takes precedence over config.mediaType
- TestArtifactType_NilManifest: nil manifest returns empty string
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.87500% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.01%. Comparing base (8b3c303) to head (013eff4).
⚠️ Report is 93 commits behind head on main.

Files with missing lines Patch % Lines
pkg/v1/partial/with.go 62.50% 1 Missing and 2 partials ⚠️
pkg/v1/remote/index.go 40.00% 1 Missing and 2 partials ⚠️
pkg/v1/remote/write.go 72.72% 1 Missing and 2 partials ⚠️

❗ There is a different number of reports uploaded between BASE (8b3c303) and HEAD (013eff4). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (8b3c303) HEAD (013eff4)
2 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2269       +/-   ##
===========================================
- Coverage   71.67%   53.01%   -18.67%     
===========================================
  Files         123      165       +42     
  Lines        9935    11226     +1291     
===========================================
- Hits         7121     5951     -1170     
- Misses       2115     4556     +2441     
- Partials      699      719       +20     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Subserial Subserial merged commit d8112ef into google:main Apr 20, 2026
17 checks passed
@malt3 malt3 deleted the referrers-api-use-correct-artifact-type branch April 20, 2026 23:06
Subserial pushed a commit to Subserial/go-containerregistry that referenced this pull request May 15, 2026
…#2269)

* Follow OCI distribution spec for artifactType and annotations

Per the OCI distribution spec (distribution-spec#395), the artifactType
on a descriptor should be set to the manifest's artifactType if present,
falling back to config.mediaType unconditionally (not only for
non-standard config types as was done previously). Additionally,
annotations from the manifest are now copied to the descriptor.

Current wording from the spec:

> Pushing Manifests with Subject
>
> When processing a request for an image manifest with the subject field,
> a registry implementation that supports the referrers API MUST respond
> with the response header OCI-Subject: <subject digest> to indicate to
> the client that the registry processed the request's subject.
>
> When pushing a manifest with the subject field and the OCI-Subject header was not set, the client MUST:
> 1. Pull the current referrers list using the referrers tag schema.
> 2. If that pull returns a manifest other than the expected image index, the client SHOULD report a failure and skip the remaining steps.
> 3. If the tag returns a 404, the client MUST begin with an empty image index.
> 4. Verify the descriptor for the manifest is not already in the referrers list (duplicate entries SHOULD NOT be created).
> 5. Append a descriptor for the pushed manifest to the manifests in the referrers list. The value of the artifactType MUST be set to the artifactType value in the pushed manifest, if present. If the artifactType is empty or missing in a pushed image manifest, the value of artifactType MUST be set to the config descriptor mediaType value. All annotations from the pushed manifest MUST be copied to this descriptor.
> 6. Push the updated referrers list using the same referrers tag schema. The client MAY use conditional HTTP requests to prevent overwriting a referrers list that has changed since it was first pulled.

With this change, we correctly follow step 5.

Changes:
- Add ArtifactType field to v1.Manifest and v1.IndexManifest structs
- Update fetchManifest to prefer manifest artifactType, fall back to
  config.mediaType, and copy manifest annotations to the descriptor
- Apply the same artifactType logic in remote/index.go, remote/write.go,
  and partial/with.go

* Add tests for artifactType and annotation handling

Test the OCI distribution spec behavior for artifactType resolution
and annotation copying:

- TestGet_ArtifactTypeAndAnnotations: end-to-end test via Get() covering
  fallback to config.mediaType, explicit artifactType precedence, and
  annotation copying to the descriptor
- TestGet_NonManifestMediaType: unparseable manifests produce no
  artifactType or annotations
- TestArtifactType_Fallback: partial.ArtifactType falls back to
  config.mediaType for standard, OCI, and custom config types
- TestDescriptor_ArtifactType_Fallback: partial.Descriptor does the same
- TestArtifactType_ExplicitArtifactType: explicit manifest artifactType
  takes precedence over config.mediaType
- TestArtifactType_NilManifest: nil manifest returns empty string
jimmidyson added a commit to mesosphere/mindthegap that referenced this pull request May 19, 2026
Bump google/go-containerregistry from v0.21.5 to v0.21.6 to include
google/go-containerregistry#2302 (transport: allow bearer realm at
same host:port as registry).

The realm-URL validation introduced in go-containerregistry#2243
(shipped in v0.21.5) rejects realms whose host resolves to a private,
loopback, or link-local IP. This is the right default for the
cross-host SSRF case (a malicious registry pointing the token
endpoint at 169.254.169.254 or a sister internal service), but it
breaks `mindthegap push bundle` (and downstream `nkp push bundle`)
against on-prem registries that colocate the registry and bearer-
token endpoint on the same private IP. e.g. for an internal Harbor
at https://10.162.182.23:5000/library the push aborts with:

    invalid realm in www-authenticate: realm host
    "10.162.182.23" is a private or link-local address

v0.21.6 keeps the cross-host SSRF block but adds a same-host:port
exception: when the realm URL host AND port match the registry, the
private-IP check is skipped. The realm cannot escape a trust boundary
the user already crossed by passing the registry reference.

The transitive bumps (docker/cli, moby/moby, docker/go-connections,
klauspost/compress, golang.org/x/{crypto,mod,net,sys,term,text,
tools}) are pulled in by go-containerregistry v0.21.6 go.mod via MVS
resolution. The containerd/stargz-snapshotter/estargz and
vbatts/tar-split indirect dependencies are dropped because v0.21.6
removed estargz support (go-containerregistry#2288).

Test expectations in images/manifest_test.go are updated for the
unrelated OCI-spec compliance change in
google/go-containerregistry#2269: mutate.AppendManifests now sets the
index entry ArtifactType to the image Config.MediaType when the image
manifest does not itself set artifactType. For the Docker schema2
fixture in TestManifestListForImage_RemoteImage this becomes
"application/vnd.docker.container.image.v1+json"
(types.DockerConfigJSON).
jimmidyson added a commit to mesosphere/mindthegap that referenced this pull request May 19, 2026
…223) (#1046)

## Summary

- Bump `github.com/google/go-containerregistry` v0.21.5 → v0.21.6 to
pick up
[google/go-containerregistry#2302](google/go-containerregistry#2302)
— transport: allow bearer realm at same host:port as registry.
- Fixes [NCN-114223](https://jira.nutanix.com/browse/NCN-114223):
`mindthegap push bundle` (and downstream `nkp push bundle`) regression
against on-prem registries that colocate the registry and bearer-token
endpoint on the same private IP. e.g. for an internal Harbor at
`https://10.162.182.23:5000/library` the push aborted with `invalid
realm in www-authenticate: realm host "10.162.182.23" is a private or
link-local address`.
- Add regression test
`TestPushDockerArchive_BearerAuthSameHostLoopbackRealm` in
`cmd/mindthegap/push/imagearchive/push_test.go` that reproduces the
exact NCN-114223 error against v0.21.5 and passes against v0.21.6.

## Background

The realm-URL validation introduced in
[go-containerregistry#2243](google/go-containerregistry#2243)
(shipped in v0.21.5) rejects realms whose host resolves to a private,
loopback, or link-local IP. This is the right default for the cross-host
SSRF case (a malicious registry pointing the token endpoint at
`169.254.169.254` or a sister internal service), but it broke legitimate
on-prem deployments that serve their own token endpoint at the same
host:port as the registry.
[#2258](google/go-containerregistry#2258)
tracked the discussion;
[#2302](google/go-containerregistry#2302)
implements the agreed fix: keep the cross-host SSRF block, but skip the
private-IP check when the realm URL host AND port match the registry
host:port.

## Transitive dependency changes

Pulled in by `go-containerregistry` v0.21.6's `go.mod` via MVS
resolution:

- `docker/cli` v29.4.0 → v29.4.3
- `moby/moby/api` v1.54.1 → v1.54.2
- `moby/moby/client` v0.4.0 → v0.4.1
- `docker/go-connections` v0.6.0 → v0.7.0
- `klauspost/compress` v1.18.5 → v1.18.6
- `golang.org/x/{crypto,mod,net,sys,term,text,tools}` — minor bumps

Dropped (v0.21.6 removed estargz support in
[go-containerregistry#2288](google/go-containerregistry#2288)):

- `containerd/stargz-snapshotter/estargz` (indirect)
- `vbatts/tar-split` (indirect)

## Test fixture update

`images/manifest_test.go` is updated for the unrelated OCI-spec
compliance change in
[go-containerregistry#2269](google/go-containerregistry#2269):
`mutate.AppendManifests` now sets the index entry `ArtifactType` to the
image's `Config.MediaType` when the image manifest does not itself set
`artifactType`. For the Docker schema2 fixture in
`TestManifestListForImage_RemoteImage` this becomes
`"application/vnd.docker.container.image.v1+json"`
(`types.DockerConfigJSON`).

## Test plan

- [x] `go test -count=1 ./...` passes (155 tests).
- [x] New `TestPushDockerArchive_BearerAuthSameHostLoopbackRealm` fails
on v0.21.5 with the exact NCN-114223 error (`invalid realm in
www-authenticate: realm host "127.0.0.1" is a private or link-local
address`) and passes on v0.21.6 — bisected to confirm it is not a
tautology.
- [x] `go build ./...` passes.
- [x] `golangci-lint run ./...` clean.
- [ ] Manual verification against an on-prem Harbor with realm host ==
registry host on an RFC1918 IP (e.g. the reproducer environment in
NCN-114223).

## Out of scope

The two pre-existing govulncheck stdlib findings (`GO-2026-4982`,
`GO-2026-4980`, `GO-2026-4971`, `GO-2026-4918` — all `go1.25.x` fixed in
`go1.25.10`) are unrelated to this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants