Skip to content

Add trusted identity to images#51737

Merged
thaJeztah merged 5 commits intomoby:masterfrom
tonistiigi:add-image-identity
Jan 16, 2026
Merged

Add trusted identity to images#51737
thaJeztah merged 5 commits intomoby:masterfrom
tonistiigi:add-image-identity

Conversation

@tonistiigi
Copy link
Member

@tonistiigi tonistiigi commented Dec 16, 2025

This PR adds a new Identity property to images visible with inspect command. This information describes the origin of the image contents, is verified by the daemon, and is immutable for the user. If the user retags the image to a different name, its identity remains immutable to the initial information.

This allows users to understand if the image content really is what it claims to be and can be used to make policy decisions.

Currently the image origin can be provided in three different ways:

  • Locally built images show their build ref that can be associated with BuildKit history API commands. Collecting this data is new and it will only be shown for images built with this PR. Depends on exporter: expose build ref to the exporter as part of buildinfo buildkit#6424
  • Pulled images show the registry location from where the image was pulled. This data was already saved for existing pulls.
  • Images that have a valid signed provenance attestation (cosign simplesigning or bundle format) are verified against trust roots and then can be inspected by the signature properties. This data is available for valid images since v29.1 . image: pull/load/save attestation manifest and signatures with image #51012 . These signatures persist when image is saved to tarball via docker save and loaded back later(to a different machine for example), and is the only way images created with docker load can currently be identified in a secure way.

Examples

Locally built image:

# docker inspect a24955b787
        "Identity": {
            "Build": [
                {
                    "Ref": "rm66qy4pboh3shq168ztcbu3j",
                    "CreatedAt": "2025-12-16T01:06:06.449507136Z"
                }
            ]
        }

Pulled image:

# docker inspect alpine
        "Identity": {
            "Pull": [
                {
                    "Repository": "docker.io/library/alpine"
                }
            ]
        }

Image signed with Docker Github Builder (currently experimental), proving artifact integrity against the source reference:

# docker image inspect tonistiigi/xx:master
        "Identity": {
            "Signature": [
                {
                    "Name": "Docker GitHub Builder Experimental (tonistiigi/xx@master)",
                    "Timestamps": [
                        {
                            "Type": "Tlog",
                            "URI": "https://rekor.sigstore.dev",
                            "Timestamp": "2025-12-04T07:42:20Z"
                        },
                        {
                            "Type": "TimestampAuthority",
                            "URI": "https://timestamp.sigstore.dev/api/v1/timestamp",
                            "Timestamp": "2025-12-04T07:42:20Z"
                        }
                    ],
                    "Signer": {
                        "CertificateIssuer": "CN=sigstore-intermediate,O=sigstore.dev",
                        "SubjectAlternativeName": "https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml@8fc70909404a502fd0eca6601b99b32fa7192b03",
                        "Issuer": "https://token.actions.githubusercontent.com",
                        "BuildSignerURI": "https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml@8fc70909404a502fd0eca6601b99b32fa7192b03",
                        "BuildSignerDigest": "8fc70909404a502fd0eca6601b99b32fa7192b03",
                        "RunnerEnvironment": "github-hosted",
                        "SourceRepositoryURI": "https://github.com/tonistiigi/xx",
                        "SourceRepositoryDigest": "a5592eab7a57895e8d385394ff12241bc65ecd50",
                        "SourceRepositoryRef": "refs/heads/master",
                        "SourceRepositoryIdentifier": "150307921",
                        "SourceRepositoryOwnerURI": "https://github.com/tonistiigi",
                        "SourceRepositoryOwnerIdentifier": "585223",
                        "BuildConfigURI": "https://github.com/tonistiigi/xx/.github/workflows/build.yml@refs/heads/master",
                        "BuildConfigDigest": "a5592eab7a57895e8d385394ff12241bc65ecd50",
                        "BuildTrigger": "push",
                        "RunInvocationURI": "https://github.com/tonistiigi/xx/actions/runs/19921023628/attempts/1",
                        "SourceRepositoryVisibilityAtSigning": "public"
                    },
                    "SignatureType": "bundle-v0.3"
                }
            ]

Manual cosign signature I created in laptop:

# docker pull tonistiigi/test:sig1
        "Identity": {
            "Signature": [
                {
                    "Name": "Self-Signed Local (GitHub: tonistiigi@gmail.com)",
                    "Timestamps": [
                        {
                            "Type": "Tlog",
                            "URI": "https://rekor.sigstore.dev",
                            "Timestamp": "2025-11-13T04:22:19Z"
                        },
                        {
                            "Type": "TimestampAuthority",
                            "URI": "https://timestamp.sigstore.dev/api/v1/timestamp",
                            "Timestamp": "2025-11-13T04:22:18Z"
                        }
                    ],
                    "Signer": {
                        "CertificateIssuer": "CN=sigstore-intermediate,O=sigstore.dev",
                        "SubjectAlternativeName": "tonistiigi@gmail.com",
                        "Issuer": "https://github.com/login/oauth"
                    },
                    "SignatureType": "bundle-v0.3"
                }
            ],
            "Pull": [
                {
                    "Repository": "docker.io/tonistiigi/test"
                }
            ]
        }

changelog:

New `Identity` field has been added to the inspect endpoint to show trusted origin information about the image. This includes build ref for locally built images, remote registry repository for pulled images, and verified signature information for images that contain a valid signed provenance attestation.

--

I think we should also add a caching layer to avoid the cost of rerunning the signature verification. Initially I thought we should add temporary opt-in for this until we have caching. But when testing I don't see any extra slowness for the user experience. I think caching is a requirement if we want to have a summary of identity information to the image list endpoint. If we want to give us more time to make potential changes in the struct I'm still ok with a temporary opt-in, but I'm not really sure if it should be in client or daemon side.

@dmcgowan @crazy-max @thaJeztah @colinhemmings

go.mod Outdated
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/go-openapi/analysis v0.24.1 // indirect
Copy link
Member Author

Choose a reason for hiding this comment

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

I managed to remove ~800k lines of dependencies from sigstore verification. This is the main part of the remaining. I don't see any quick way to get rid of it unfortunately. Sigstore folks were ok to consider alternatives but this would be quite fundamental change in their upstreams.

module github.com/moby/moby/v2

go 1.24.3
go 1.25.0
Copy link
Member Author

Choose a reason for hiding this comment

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

This was unfortunately forced on us in recent sigstore update (that we need to reduce dependencies) moby/policy-helpers#12 (review)

Comment on lines +1384 to +1386
v, err := policyverifier.NewVerifier(policyverifier.Config{
StateDir: confDir,
})
Copy link
Member

Choose a reason for hiding this comment

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

I think we should be able to configure the update interval on daemon side and have a good default? Maybe require online as well? https://github.com/moby/policy-helpers/blob/9fcc1a9ec5c9573385c30ab81cac41e9da124f0e/verifier.go#L28-L29

Copy link
Member Author

Choose a reason for hiding this comment

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

Not against, but I think these are sort of advanced features that could be added later in a follow up.

@tonistiigi tonistiigi marked this pull request as ready for review January 12, 2026 17:39
Comment on lines 148 to 158
// DockerReference is the Docker image reference associated with the signature.
// This is an optional field only present in older hashedrecord signatures.
DockerReference string `json:"DockerReference,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

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

What's a "Docker image reference"? Is that a "distribution" reference (name / tag) of the image? https://pkg.go.dev/github.com/distribution/reference

Wouldn't that always match the image that's inspected, or is this for situations where an image is tagged under multiple names?

Wondering if this should be a more generic term (not sure if we should consider this "docker" if it's defined by distribution 🤔)

Copy link
Member Author

Choose a reason for hiding this comment

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

This comes from https://github.com/containers/image/blob/a5061e5a5f00333ea3a92e7103effd11c6e2f51d/docs/containers-signature.5.md#criticalidentitydocker-reference .

This is actually an old signature format, only used by DHI atm for new images.

Wouldn't that always match the image that's inspected, or is this for situations where an image is tagged under multiple names?

No, as this is part of the reference, then it is immutable. It is like the name the signer gave to the image while singing. Eg. in DHI this is the upstream name of the image. If you mirror or copy DHI image under your org, this will remain the upstream name. So even if you pulled from your org, the signature can confirm that this is still the same image that was originally tagged as specific DHI name.

Copy link
Member

Choose a reason for hiding this comment

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

This comes from https://github.com/containers/image/blob/a5061e5a5f00333ea3a92e7103effd11c6e2f51d/docs/containers-signature.5.md#criticalidentitydocker-reference .

This is actually an old signature format, only used by DHI atm for new images.

Yeah it is recommended to use the Sigstore Bundle Format instead. Simple Signing is kinda deprecated now.

Comment on lines 89 to 92
identity, err := i.imageIdentity(ctx, target.Digest)
if err != nil {
log.G(ctx).WithError(err).Warn("failed to determine Identity property")
}
Copy link
Member

Choose a reason for hiding this comment

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

Given that this information is a new addition, and (technically) not available in older API versions, should we add an option to ImageInspectOpts to make inclusion optional (and gate it by API version?)?

// ImageInspectOpts holds parameters to inspect an image.
type ImageInspectOpts struct {
Manifests bool
Platform *ocispec.Platform
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so. I think it is good that this doesn't require CLI side changes. The output is already a block of JSON for this specific API.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, sorry, I was probably not clear; this is a backend option (so not user-facing) my thinking here was to add a boolean, then in the router set that boolean to true for API >= 1.53

Copy link
Member Author

Choose a reason for hiding this comment

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

That still means that it would require new CLI I think. I think it is nice that there aren't extra limitations like that (@colinhemmings). This is not an API where there would be some backward compatibility issues or changes in the current fields.

Copy link
Member

Choose a reason for hiding this comment

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

Older clients won't unmarshal it because it's not in the types they use, so the information won't be there, unless they dump the raw response.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's fine just to version gate it and just set the Identity to nil on older versions.

This way we avoid complicating the backend implementation but still keep the backwards compatibility.

Copy link
Member Author

Choose a reason for hiding this comment

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

Older clients won't unmarshal it because it's not in the types they use, so the information won't be there, unless they dump the raw response.

No, this is not the case. I haven't built any new CLI in anywhere this has been tested. It would be impossible to use it atm if this was the case.

Copy link
Member

Choose a reason for hiding this comment

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

The CLI dumps the raw JSON, but clients following the API spec, or implementations using the unmarshaled response as returned by the client won't have the info.

identity := &imagetypes.ImageIdentity{}

for k, v := range info.Labels {
if ref, ok := strings.CutPrefix(k, exporter.BuildRefLabel); ok {
Copy link
Member

Choose a reason for hiding this comment

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

Format here is something like moby/build.ref.docker.io/library/ubuntu:latest ? Is that also because the image can be tagged under different names?

I was curious why the ref wasn't part of the JSON value that's stored in the label 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not distribution reference. BuildRef is just a unique identifier for the build. You can make extra requests against buildkit with the identifier to extract more data.

I was curious why the ref wasn't part of the JSON value that's stored in the label 😅

There can be multiple refs for the same image contents. Putting it to the same value would mean it needs to be reencoded on each modification and creates race conditions and possible size limits. Easier if each build just adds own label.

@tonistiigi tonistiigi requested a review from thaJeztah January 13, 2026 17:58
@tonistiigi tonistiigi added the kind/feature Functionality or other elements that the project doesn't currently have. Features are new and shiny label Jan 13, 2026
Copy link
Member

@crazy-max crazy-max left a comment

Choose a reason for hiding this comment

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

LGTM

For TUF state/gc we can look at it as follow-up.

Can you PTAL @thaJeztah? Specially the api/swagger part? 🙏

@thompson-shaun thompson-shaun modified the milestones: 29.3.0, 29.2.0 Jan 14, 2026
@thaJeztah
Copy link
Member

rebased after #51857 was merged, and moved some touch-up diffs to the right commit; need a slight cleanup / squash (the renames); I'll have a look at that later.

@thaJeztah thaJeztah force-pushed the add-image-identity branch 2 times, most recently from 9710305 to ecacf5f Compare January 16, 2026 12:40

// SignerIdentity contains information about the signer certificate used to sign the image.
// This is certificate.Summary with deprecated fields removed and keys in Moby uppercase style.
// This is [certificate.Summary] with deprecated fields removed and keys in Moby uppercase style.
Copy link
Member

Choose a reason for hiding this comment

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

I noticed the "and keys in Moby uppercase style" ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Agh, this got lost when all the fields were copied again in #51737 (comment)

Comment on lines 9 to 17
Copy link
Member

Choose a reason for hiding this comment

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

But that only applies to the first 3 fields, but not for the other fields, which are lowercase; was that intentional?

Also curious what the //nolint:tagliatelle was for; is that because of the mixed upper/lowercase ?

edit: ah, probably copied from https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg/fulcio/certificate#Extensions

@thaJeztah
Copy link
Member

Squashed the last 3 commits with the first one.

@thaJeztah
Copy link
Member

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

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

LGTM

but could use another review, because I pushed the last changes

Copy link
Contributor

@vvoland vvoland left a comment

Choose a reason for hiding this comment

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

LGTM, but a follow up with some integration tests for this wouldn't hurt 😅

@thaJeztah
Copy link
Member

Issue / glitch with docker hub auth, or just another GitHub actions glitch?

time="2026-01-16T15:08:02Z" level=info msg="fetch failed after status: 404 Not Found" host="localhost:64203"
time="2026-01-16T15:08:32Z" level=info msg="fetch failed" error="failed to authorize: failed to fetch oauth token: unexpected status from POST request to https://auth.docker.io/token: 504 Gateway Timeout" host=registry-1.docker.io

@vvoland
Copy link
Contributor

vvoland commented Jan 16, 2026

Looks like Hub

@thaJeztah
Copy link
Member

Yup; I see there's an incident; not sure if they already updated the status page though.

@thaJeztah
Copy link
Member


// Reference to specific build instructions that are responsible for signing.
BuildSignerURI string `json:"buildSignerURI,omitempty"` // 1.3.6.1.4.1.57264.1.9
BuildSignerURI string `json:"BuildSignerURI,omitempty"` // 1.3.6.1.4.1.57264.1.9
Copy link
Member

Choose a reason for hiding this comment

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

@tonistiigi can you squash these case changes with the first commit, and I think the second commit also needs to be updated as the Swagger may be using lowercase.

Or should we make all fields match the upstream (and lowercase)? Not sure if it's a standard / spec that we should match?

Copy link
Member Author

Choose a reason for hiding this comment

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

Should be ok now.

Or should we make all fields match the upstream (and lowercase)?

It looks out of place in Inspect output then as all other fields are uppercase.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Enable inspect endpoint to verify image signatures
and expose signature information for inspection.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

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

LGTM

@thaJeztah thaJeztah merged commit 397c7d6 into moby:master Jan 16, 2026
240 of 242 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/builder/buildkit Build area/daemon Core Engine area/docs area/images Image Distribution containerd-integration Issues and PRs related to containerd integration impact/api impact/changelog kind/feature Functionality or other elements that the project doesn't currently have. Features are new and shiny module/api

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants