Complete OCI 1.1 Referrers API Support Across All Cosign Commands
Description
We currently push attestations to container registries using tags. I want to enable our builds to start pushing all attestations using the referrer's API but there are some tooling gaps in being able to download and verify this metadata.
Commands WITH OCI 1.1 Support:
-
cosign tree---registry-referrers-mode=oci-1-1+--experimental-oci11 -
cosign sign---registry-referrers-mode=oci-1-1 -
cosign attach sbom---registry-referrers-mode=oci-1-1 -
cosign verify---experimental-oci11
Commands MISSING OCI 1.1 Support:
-
cosign download attestation -
cosign download signature -
cosign download sbom -
cosign verify-attestation -
cosign attach signature -
cosign attach attestation -
cosign attest(creates and attaches new attestations)
I think that we should have universal support for the OCI 1.1 referrer's API. The inconsistency here is confusing. When proposing how to make Chains use the referrer's API, I didn't realized that I ended up trying to use the new bundle format instead of just changing the storage mechanism.
All endpoints should ideally have options such that it would be possible to achieve all of the following:
# Download commands get OCI 1.1 support
cosign download attestation --experimental-oci11 example.com/app:v1.0
# Verify attestation gets OCI 1.1 support
cosign verify-attestation --experimental-oci11 example.com/app:v1.0
# Attach commands get OCI 1.1 support
cosign attach signature --experimental-oci11 --signature=sig.txt example.com/app:v1.0
# Create commands get OCI 1.1 support
cosign attest --experimental-oci11 --predicate=predicate.json --type=slsaprovenance example.com/app:v1.0
cosign sign --experimental-oci11 --key=cosign.key example.com/app:v1.0
I see two potential approaches that address different priorities. Is there a preference between these approaches? I feel like I likely don't understand the full complexity of the latter approach, but I also feel like it is the better approach as I expect it would/could be more consistent with future bundle support as well. The first approach would definitely be simpler to implement.
Option A: Extend the current pattern
The current implementations just handle interfacing with the referrer's API directly within the functions. For example, verify and tree use ociremote.Referrers() while attach sbom calls mutate.Subject().
If we were to leverage this pattern, there would likely be duplicated code across multiple subcommands. This would work, is likely going to be faster, and would have fewer potential side effects.
Option B: Create a unified abstraction for managing artifact operations
In looking at the code, it seems like we have many locations where three different types of operations are performed: find, attach, and create. If we extract these all into a centralized artifact management abstraction then we could potentially get a benefit of code reuse across them (i.e. in a new pkg/oci/artifacts/ package).
This approach could theoretically also simplify cosign's use as a dependency.
Its primary interface would be something like this:
// Unified interface for all artifact types
type ArtifactManager interface {
FindArtifacts(ctx context.Context, subject name.Digest, artifactType string, filters ...Filter) ([]Artifact, error)
AttachArtifact(ctx context.Context, subject name.Digest, artifact Artifact, opts AttachOptions) error
CreateArtifact(ctx context.Context, subject name.Digest, content []byte, artifactType string, signingOpts SigningOptions, attachOpts AttachOptions) error
}
This would let the CLI implementations follow patterns to defer common work to the manager
// CLI command implementations using the unified manager:
func DownloadAttestationCmd(...) error {
manager, _ := artifacts.NewArtifactManager(storageMode, format, regOpts)
filters := []Filter{PredicateTypeFilter(predicateType)}
artifacts, err := manager.FindArtifacts(ctx, digest, "att", filters...)
// output results
}
func AttachSBOMCmd(...) error {
manager, _ := artifacts.NewArtifactManager(storageMode, format, regOpts)
artifact := Artifact{Type: "sbom", Content: sbomData, MediaType: mediaType}
return manager.AttachArtifact(ctx, digest, artifact, opts)
}
func AttestCmd(...) error {
manager, _ := artifacts.NewArtifactManager(storageMode, format, regOpts)
return manager.CreateArtifact(ctx, digest, predicate, "att", signingOpts, attachOpts)
}
I think that this approach should be able to be reused for bundles which might also simplify planning related to v3.
Yes! I ran into this while recently working on #3927.
What we learned in migrating commands over to use the protobuf bundle format (#3139) is that a smooth way to do so is to default to the new behavior, but have a fallback that detects if you're using the old behavior (especially for verification commands). In fact, that's exactly what https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md#retrieval recommends.
So I would love to see us take our existing OCI retrieval code in cosign, update it to use what the cosign bundle spec recommends, and remove the various oci11 flags, since we'll use it where supported but fall back when it isn't.
@steiza , that would be a change that happens in v3 though? We can make the change to have a consistent interface when interacting with the OCI registry in v2 where we default to the "legacy" tag-based method and then in v3, we can switch the default to the referrer's API?
I'd recommend that verification should not require an extra flag. If no option is given, have the command search through any available storage methods to see if the signed content can be found anywhere. That allows build tooling to change without forcing every consumer to simultaneously update their verification scripts and apps.
@sudo-bmitch , do you think that should apply for commands like tree as well?
@sudo-bmitch , do you think that should apply for commands like
treeas well?
Yes, everything that consumes the artifacts can have an automatic fallback vs tools that producing them would specify which type to create. That also gives the flexibility to change the default type of artifact produced in the future with a minimal impact to users (only affecting those running older unsupported versions).
I created a draft pull request with the abstraction implementation only to reduce the number of changes that might need to be reviewed at once: https://github.com/sigstore/cosign/pull/4336
I had a good call with @steiza and @haydentherapper yesterday. The main points after reflecting on the conversation are:
- Wherever possible, sigstore-go should be used as a dependency when container signing isn't needed.
- Cosign is trying to position itself as a tool/API for container signing.
- While Cosign currently supports the legacy signature/attestations, we want to move to using bundles as the default (i.e. for v3)
Generating signatures and attestations with the protobuf bundle format only supports the code path using the referrer's API. Maybe we should consider supporting the tag-based approach for that, but the question is separate from the current one. I am not sure what the existing registry support is for the referrer's API.
That being said, there is a desire to move to the cosign v3 soon, so there are likely to only be minimal changes before the community focuses only on changes for it. Since v3 would move to use the protobuf bundle format by default (and therefore the referrer's API), anyone that would want current functionality and support should continue to use v2.
As @steiza highlighted in #4354 , we have a lot of options available for the various commands currently. Each of these options requires a certain amount of maintenance burden. The sentiment that I get from there is that we want to carefully consider any additional flags that we add to commands.
So in summary, I think it makes sense to close #4336 (I already did that). I think that it also makes sense to limit the immediate changes to focus on the use of cosign as a library and just add functionality to pkg/oci/remote/write.go to abstract the referrer's logic from WriteAttestationNewBundleFormat to its own exported function (WriteReferrer) and then use that in the bundle generation flow. We could also add a WriteAttestationNewBundleFormat function in addition to or instead of the WriteReferrer function. While I didn't propose to update any of the generating CLI commands with this functionality, it can certainly be considered if the community wants to include that too. I opened #4357 with these changes so that we can see how big they are.
This creates a slight separation between cosign as a binary and as an API. For the binary, we can be opinionated and focus on improving the default. As an API, we still focus on moving towards those defaults, but we also help any dependencies (i.e. Chains) iteratively work towards that "better tomorrow" where we can switch to using the referrers's API and the bundle format, but while also controlling those levers separately.
In the future, we should still:
- Determine whether protobuf bundles can be pushed using the tag-based referrer's api
- Ensure that all functions which fetch attestations can support fetching both referrer modes as well as both bundle formats