Skip to content

Conversation

@tonistiigi
Copy link
Member

fixes #5561

This adds a new exporter option oci-artifact. If set then it changes the attestation manifest to be exported with following changes:

  • subject field is added to attestation manifest
  • config uses empty descriptor instead of fake image config
  • artifactType property is set

The intention is to change this behavior from opt-in to opt-out in some future release when target is OCI image.

Example https://explore.ggcr.dev/?image=docker.io/tonistiigi/buildkit@sha256:bf8355824354aee83a9874f7178ea91a17752603a949e89f00f9e3ca5404db35&mt=application%2Fvnd.oci.image.manifest.v1%2Bjson&size=915

Quick test:

docker buildx create --bootstrap --name=dev --driver-opt image=tonistiigi/buildkit:artifact-mfst

This adds a new exporter option oci-artifact. If set then it changes
the attestation manifest to be exported with following changes:
- subject field is added to attestation manifest
- config uses empty descriptor instead of fake image config
- artifactType property is set

The intention is to change this behavior from opt-in to opt-out
in some future release when target is OCI image.

Signed-off-by: Tonis Tiigi <[email protected]>
@tonistiigi
Copy link
Member Author

cc @wieringen @tianon @dvdksn @sudo-bmitch @cdupuis

@sudo-bmitch
Copy link

sudo-bmitch commented Dec 6, 2024

Thanks for getting this started! There's some fallback logic the spec requires when this is pushed to a registry without the referrers API. We have that documented at: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests-with-subject

ETA: here's what that fallback tag would look like with a single entry: https://explore.ggcr.dev/?image=sudobmitch%2Fdemo:sha256-bc2046336420a2852ecf915786c20f73c4c1b50d7803aae1fd30c971a7d1cead

@tianon
Copy link
Member

tianon commented Dec 6, 2024

I see this as more of an intermediate change -- the attestation still ends up in the index, which is the current means of discoverability, it's just now annotated with an appropriate subject so that when referrers are implemented, it shows up in the API (and so that the link between the attestation and its manifest is clear in the data model). Once referrers are implemented more universally (especially Docker Hub, but perhaps others?), then we can think about removing them from the index too. In other words, I don't see a reason to implement the fallback tags here.

(IMO the spec ought to say SHOULD there instead of MUST, but that comes from a place of thinking the fallback is a really poor compromise for UX and the upsides don't outweigh the down. To be very explicit, if buildkit/buildx implements the fallback behavior, I'll be patching it out in my own builds and/or pushing in a way that doesn't involve the fallback logic.)

@sudo-bmitch
Copy link

We had made it a "must" because registries are not required to support the referrers response for content pushed before the server is upgraded if it doesn't also have the fallback tag. If buildkit pushes content without the fallback support, it may not be discovered when a registry later supports the referrers API.

This was a concession for large registries like Docker Hub that may have a lot of content they don't want to rescan. Registries can assume well behaved clients, and only need to rescan the small percentage of manifests referenced by a fallback tag. If large registries no longer have the concern about rescanning all of the pre-existing content, we could propose a change to the spec from a "client must" to a "registry must".

Ref: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#enabling-the-referrers-api

@cdupuis
Copy link
Collaborator

cdupuis commented Dec 7, 2024

Thanks @tonistiigi. This is a great first step. Making this opt-in is a good solution to prevent registries that don't yet support/accept a subject field in the manifest to reject the image.

Implementing the fallback mechanism and eventually using that for DOI (although @tianon indicated he wouldn't, so this is theoretical) would have quite a few negative effects and surprises for our users on Docker Hub.

Again, great addition.

@tonistiigi tonistiigi merged commit fe09265 into moby:master Jan 10, 2025
96 checks passed
@polarathene
Copy link
Contributor

polarathene commented Mar 5, 2025

EDIT: Skip to next comment message. (UPDATE: Raised a separate issue at buildx)


Was this feature meant to remove the platform key usage with unknown/unknown? I've tried to use it based on what documentation I could come across but I'm not seeing any difference.


Assumed usage

buildx v0.21.0 has BuildKit 0.20.0 (this PR was for BuildKit 0.19.0).

I realize there are docs added related to the feature, but when attempting to leverage it, the manifest index created does not seem to have any difference from oci-artifact=true?:

# NOTE: `--attest type=provenance,mode=min` is implicit by default
$ docker buildx build \
  --output type=image,push=true,oci-mediatypes=true,oci-artifact=true \
  --platform linux/amd64,linux/arm64 \
  --tag ghcr.io/polarathene/example:test \
  .

 => => pushing layers
 => => pushing manifest for ghcr.io/polarathene/example:test@sha256:a5dddf803c71cbad6fc45ebf1931584213562a2f4184a6c42e27a6312907aa5f

I have tried this on a fresh VPS install with Fedora 41 and Docker repo added for packages.

Docker Engine: 28.0.1
Buildx: v0.21.1

Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
Cgroup Driver: systemd
Cgroup Version: 2
Kernel Version: 6.12.9-200.fc41.x86_64

Attempted to use the inspection tooling with containerd image storage, but apparently that only works with an actual registry? So I've tried with ghcr.io, still no luck.


Reproduction

This issue was closed when this PR was merged. I assume the output was meant to differ, but my attempt to reproduce with the above usage appears to be the same.

I can replicate all those steps as shown below, but I assumed this feature was meant to fix the original manifest index published?

Click to view reproduction

Query manifests via crane instead as docker manifest inspect / docker buildx imagetools inspect omit output? (UPDATE: docker buildx imagetools inspect --raw seems to be equivalent to crane manifest?)

$ curl -fsSL https://github.com/google/go-containerregistry/releases/download/v0.20.3/go-containerregistry_Linux_x86_64.tar.gz | tar -xz crane

$ ./crane manifest ghcr.io/polarathene/example:test@sha256:a5dddf803c71cbad6fc45ebf1931584213562a2f4184a6c42e27a6312907aa5f

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:6f81b18808466808136cd43e68a156f7a58937bd4e50edacce158ac5300cbce5",
      "size": 668,
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4",
      "size": 668,
      "platform": {
        "architecture": "arm64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:6f1a7b24844a8ff6314eaa7fe99432de112731ef4e3a3811f58f07e062beea2d",
      "size": 914,
      "annotations": {
        "vnd.docker.reference.digest": "sha256:6f81b18808466808136cd43e68a156f7a58937bd4e50edacce158ac5300cbce5",
        "vnd.docker.reference.type": "attestation-manifest"
      },
      "platform": {
        "architecture": "unknown",
        "os": "unknown"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:47fe6f33b043ac8b3999f3d76b4d5cc46a8c4dcff4a4eef5f1a9e826ef96c988",
      "size": 914,
      "annotations": {
        "vnd.docker.reference.digest": "sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4",
        "vnd.docker.reference.type": "attestation-manifest"
      },
      "platform": {
        "architecture": "unknown",
        "os": "unknown"
      }
    }
  ]
}

Last entry from prior output is unknown/unknown marked as attestation-manifest, query by digest:
(NOTE: The tag :test isn't relevant/required when referencing digest directly)

$ ./crane manifest ghcr.io/polarathene/example:test@sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:b418f2217afb0de9b0b74a40e0407264d7986883dae7cc317adef8249b67db93",
    "size": 837
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:6e771e15690e2fabf2332d3a3b744495411d6e0b00b2aea64419b58b0066cf81",
      "size": 3993029
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:103341866df2e1f713ff4439251d77dc8ab241ef0ed0647ff96c517aab401307",
      "size": 139
    }
  ]
}

Use oras to attach an artifact:

$ curl -fsSL https://github.com/oras-project/oras/releases/download/v1.2.2/oras_1.2.2_linux_amd64.tar.gz | tar -xz oras

$ echo "Hello, world!" > hi.txt
$ ./oras attach --artifact-type doc/example ghcr.io/polarathene/example:test@sha256:a5dddf803c71cbad6fc45ebf1931584213562a2f4184a6c42e27a6312907aa5f hi.txt

✓ Exists    application/vnd.oci.empty.v1+json
  └─ sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
✓ Uploaded  hi.txt
✓ Uploaded  application/vnd.oci.image.manifest.v1+json
✓ Uploaded  application/vnd.oci.image.manifest.v1+json
Attached to [registry] ghcr.io/polarathene/example:test@sha256:a5dddf803c71cbad6fc45ebf1931584213562a2f4184a6c42e27a6312907aa5f
Digest: sha256:5539a7809e319343ea450ad87eb4520c071fe0aad6987d4fcf4c3ea7f1175ebd

oras published this manifest index with a tag referencing the target image by it's digest (which the attachment is associated to).

  • This isn't conveyed in the output above, nor in the linked example I'm reproducing here.
  • Presumably this content was meant to be updated on the original image asset that the digest tag is associated to? (UPDATE: GHCR does not presently support the Referrers API, so the Referrer Tag schema was used as fallback - detailed comparison and documentation)
$ ./crane manifest ghcr.io/polarathene/example:sha256-a5dddf803c71cbad6fc45ebf1931584213562a2f4184a6c42e27a6312907aa5f | jq .

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:5539a7809e319343ea450ad87eb4520c071fe0aad6987d4fcf4c3ea7f1175ebd",
      "size": 722,
      "annotations": {
        "org.opencontainers.image.created": "2025-03-05T04:48:31Z"
      },
      "artifactType": "doc/example"
    }
  ]
}

Querying that digest from the output of that index or the oras CLI output, is a manifest for the artifact that oras uploaded, and a subject referencing the original image index that the oras CLI was provided with:

$ ./crane manifest ghcr.io/polarathene/example@sha256:5539a7809e319343ea450ad87eb4520c071fe0aad6987d4fcf4c3ea7f1175ebd | jq .

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "doc/example",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2,
    "data": "e30="
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar",
      "digest": "sha256:d9014c4624844aa5bac314773d6b689ad467fa4e1d1a50a1b8a99d5a95f72ff5",
      "size": 14,
      "annotations": {
        "org.opencontainers.image.title": "hi.txt"
      }
    }
  ],
  "subject": {
    "mediaType": "application/vnd.oci.image.index.v1+json",
    "digest": "sha256:a5dddf803c71cbad6fc45ebf1931584213562a2f4184a6c42e27a6312907aa5f",
    "size": 1607
  },
  "annotations": {
    "org.opencontainers.image.created": "2025-03-05T04:48:31Z"
  }
}

Expectation

EDIT: See next comment with proper expected output. (UPDATE: Proper expectation)

Invalid ignore

This portion of the original image index:

{
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "digest": "sha256:47fe6f33b043ac8b3999f3d76b4d5cc46a8c4dcff4a4eef5f1a9e826ef96c988",
  "size": 914,
  "annotations": {
    "vnd.docker.reference.digest": "sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4",
    "vnd.docker.reference.type": "attestation-manifest"
  },
  "platform": {
    "architecture": "unknown",
    "os": "unknown"
  }
}

Should be similar to this when using oci-artifact=true?:

{
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "digest": "sha256:5539a7809e319343ea450ad87eb4520c071fe0aad6987d4fcf4c3ea7f1175ebd",
  "size": 722,
  "annotations": {
    "org.opencontainers.image.created": "2025-03-05T04:48:31Z"
  },
  "artifactType": "doc/example"
}

That would remove the platform key, and use "artifactType": "attestation-manifest" instead of the docker specific annotations?

Perhaps I've tried to use the feature incorrectly. I've tried a variety of things, but the docs are a bit sparse on this.

@polarathene
Copy link
Contributor

polarathene commented Mar 6, 2025

TL;DR: Regardless of the oci-artifact value, the image index is effectively the same. I assumed it would also be adapted to OCI 1.1 with artifactType fields? (as shown via collapsed JSON at the end of this comment)


JSON outputs via any raw manifest command:

  • crane manifest ghcr.io/polarathene/example:test
  • oras manifest fetch ghcr.io/polarathene/example:test
  • docker buildx imagetools inspect --raw ghcr.io/polarathene/example:test
  • docker manifest inspect (Lacks raw inspection, omits fields from retrieved JSON)
Click to view JSON
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:6f81b18808466808136cd43e68a156f7a58937bd4e50edacce158ac5300cbce5",
      "size": 668,
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4",
      "size": 668,
      "platform": {
        "architecture": "arm64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:6f1a7b24844a8ff6314eaa7fe99432de112731ef4e3a3811f58f07e062beea2d",
      "size": 914,
      "annotations": {
        "vnd.docker.reference.digest": "sha256:6f81b18808466808136cd43e68a156f7a58937bd4e50edacce158ac5300cbce5",
        "vnd.docker.reference.type": "attestation-manifest"
      },
      "platform": {
        "architecture": "unknown",
        "os": "unknown"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:47fe6f33b043ac8b3999f3d76b4d5cc46a8c4dcff4a4eef5f1a9e826ef96c988",
      "size": 914,
      "annotations": {
        "vnd.docker.reference.digest": "sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4",
        "vnd.docker.reference.type": "attestation-manifest"
      },
      "platform": {
        "architecture": "unknown",
        "os": "unknown"
      }
    }
  ]
}

Query the digest for the first entry (of the above output returned) with the attestation-manifest type, and cat that digest result to next crane/imagetools query:

crane manifest ghcr.io/polarathene/example:test \
  | jq -r '[ .manifests[]
    | select(.annotations | has("vnd.docker.reference.digest"))
    | .digest
  ] | first' \
  crane manifest "ghcr.io/polarathene/example@$(cat -)"

# Results in the equivalent of:
crane manifest ghcr.io/polarathene/example@sha256:6f1a7b24844a8ff6314eaa7fe99432de112731ef4e3a3811f58f07e062beea2d

Now you'll get one of these outputs depending on oci-artifact value:

oci-artifact=false:

Click to view JSON
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:50dc03b6fbf201ebc7f100aceba6733b588b162b620c2bff62e62130eac44c37",
    "size": 167
  },
  "layers": [
    {
      "mediaType": "application/vnd.in-toto+json",
      "digest": "sha256:00ac79d774183fefad81b466e1c69de1933917bd7cf6257b593c756e38215635",
      "size": 1014,
      "annotations": {
        "in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2"
      }
    }
  ]
}

oci-artifact=true:

Click to view JSON
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.docker.attestation.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2,
    "data": "e30="
  },
  "layers": [
    {
      "mediaType": "application/vnd.in-toto+json",
      "digest": "sha256:c285ccb8f717d177ace6b50792a21989874996ecb0a2f522d933437032b92290",
      "size": 1014,
      "annotations": {
        "in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2"
      }
    }
  ],
  "subject": {
    "mediaType": "application/vnd.oci.image.manifest.v1+json",
    "digest": "sha256:6f81b18808466808136cd43e68a156f7a58937bd4e50edacce158ac5300cbce5",
    "size": 668,
    "platform": {
      "architecture": "amd64",
      "os": "linux"
    }
  }
}

So oci-artifact=true is working, I just misunderstood the scope?

Expectation

I was under the belief that the image index published would have also been adapted similar to the documented example at the Cosign Bundle Specification, which would look like this? (UPDATE: Expected image index JSON):

Click to view JSON (ignore this is incorrect)
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:6f81b18808466808136cd43e68a156f7a58937bd4e50edacce158ac5300cbce5",
      "size": 668,
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:c921e8c46326db1dbd537eaf8d9566408497e105d20a5f05baeaab09afff54b4",
      "size": 668,
      "platform": {
        "architecture": "arm64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:6f1a7b24844a8ff6314eaa7fe99432de112731ef4e3a3811f58f07e062beea2d",
      "artifactType": "application/vnd.docker.attestation.manifest.v1+json"
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:47fe6f33b043ac8b3999f3d76b4d5cc46a8c4dcff4a4eef5f1a9e826ef96c988",
      "artifactType": "application/vnd.docker.attestation.manifest.v1+json"
    }
  ]
}

"golang.org/x/sync/errgroup"
)

const attestationManifestArtifactType = "application/vnd.docker.attestation.manifest.v1+json"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add subject to attestation manifests

7 participants