Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image destination transport support for sigstore's cosign image signing #1364

Closed
wants to merge 13 commits into from

Conversation

font
Copy link

@font font commented Aug 27, 2021

This is a very rough WIP PR to add image destination transport support for signing container images using the cosign signature spec. There are some TODOs throughout and summarized here:

TODO:

  • xdg-open does not open browser and URL is not printed to terminal. This prevents successful keyless OIDC auth flow in rootless mode. Additionally, callback server at http://localhost:5556/auth/callback is not successfully launched. Need to continue investigating this issue.
  • Push the signature image and manifest (putSignature) to the registry.
  • Eventually (not necessarily in this PR) remove use of env vars for Rekor and Fulcio server URLs and replace with policy configuration.
  • Add signing to copyMultipleImages.
  • Use debug log in sigstore mechanism.
  • registries.d/default.yaml field name and location for storing Sigstore's cosign signatures.
  • Consider adding optional claims to the Optional simple signing field.
  • Consistent usage of sigstore/cosign naming convention throughout. The c/* tooling already has a defined meaning for a sigstore concept. Converge on proper naming to differentiate both.
  • Consolidate interfaces and APIs with existing GPG signing mechanism.
  • Any useful tests.
  • Add missing godoc comments.
  • Anything else?

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

Just a fairly surface-level skim for now.

I’m strongly focusing on the public API because Go module semantics requires us to not break it, so it’s fairly important to get it right the first time.

As far as the internal impact on c/image’s copy.Image/PutSignature/policy.json and so on, there isn’t much here yet, it seems.

copy/copy.go Outdated
@@ -123,6 +123,7 @@ type ImageListSelection int
// Options allows supplying non-default configuration modifying the behavior of CopyImage.
type Options struct {
RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature.
Sign bool // Adds cosign signature using ephemeral keys.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Something vaguely like CosignSign? We’ll need to start being explicit about which one the caller wants. Anyway, there are quite a few other parameters to (optionally?) provide — so maybe a pointer to a struct that contains all of them.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I think we need to discuss what are all possible Cosign signing options we can foresee adding in the future. As a result, I think it makes sense to expand this to a struct.

copy/sign.go Outdated
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports"
"github.com/pkg/errors"
)

// createSignature creates a new signature of manifest using keyIdentity.
func (c *copier) createSignature(manifest []byte, keyIdentity string) ([]byte, error) {
if keyIdentity == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAICS the only caller knows which one of these is the case, so it can just call one of the two directly.

Copy link
Member

Choose a reason for hiding this comment

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

I'd echo that. Would make callers easier to read (like prose).

copy/sign.go Outdated
return nil, errors.Wrap(err, "getting key from Fulcio")
}

fmt.Println("Marshalling payload into JSON")
Copy link
Collaborator

Choose a reason for hiding this comment

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

These should go to the copy reportWriter

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 some/most of them should be logrus.Debug. I wouldn't understand what "Marshalling payload into JSON" would mean when seeing that during podman pull ... so many payloads and too much JSON everywhere :^)

Copy link
Author

Choose a reason for hiding this comment

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

Ack, mostly just debug stuff for now.

copy/sign.go Outdated
}

fmt.Println("Generating certificate from Fulcio")
if err := mech.GenerateCertificate(); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Having GenerateCertificate down here is probably not quite the right place.

It’s not trivially clear to me that having an implied state in mech is preferable; it well might be the best design — just something to think about more. Maybe the certificate should be an explicitly caller-managed object? Or we should have separate verification/signing mechanisms, with signing mechanisms implicitly creating a private key on construction?

E.g.

  • Would this ever support non-ephemeral keys that the caller just points to, comparable to GPG keys?
  • Alternatively, do we want to generate an exactly single-use key, so that GenerateCertificate is implied by Sign?
  • I guess ^^^ not — consider signing a multi-arch image = sign all the per-arch images, and maybe the top-level index as well: in that case we almost certainly want the user to only have one OIDC authentication, and I guess, to only use one private key for all of of the component images. So, the copy structure must be significantly different from this, we can’t do an OIDC request down here in per-component-image signature creation.

// the usage of the BaseURL is defined under docker/distribution registries—separate storage of docs/signature-protocols.md
// Warning: This function only exposes configuration in registries.d;
// just because this function returns an URL does not mean that the URL will be used by c/image/docker (e.g. if the registry natively supports X-R-S-S).
func CosignStorageBaseURL(sys *types.SystemContext, ref types.ImageReference, write bool) (*url.URL, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is all of this local storage intended to exist in the final version? From a very quick glance it seems basically copy&pasted.

I was assuming, without it ever being discussed, that the signatures would be stored only in the registry using the tag convention, and there wouldn’t be any lookaside mechanism like this. If you do plan to have a lookaside mechanism in the final version (and I assume Cosign doesn’t already define one that copy&pastes the simple signing one), I’d much prefer to think about how to have one mechanism that can store both kinds of signatures; maybe a new naming convention, or perhaps even a new format, that includes a signature type indication along with the signature.

[It is also expected to ~work for the existing lookaside mechanism to store multiple kinds of signatures, because existence of invalid signatures is not a reason to reject an image; so storing Cosign signatures and simple signing signatures in a single look aside should just work, with each policy requirement picking its own kinds of signatures. So, strictly speaking, we might not need any changes to the look aside at all.]

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it's mostly duplicated, with the underlying paths differing between cosign and the original simple signing look aside location. I was trying an expedient way to differentiate between the two formats in the look aside, and thought having a separate path may be the easiest for now since there's no policy configuration for cosign yet. I'll need to think through and discuss this with you further.


// TODO: use debug log
fmt.Println("Generating ephemeral keys")
priv, err := generatePrivateKey()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Besides being a good practice, AFAIK FIPS requires explicit zeroization of all secret material after it is no longer used. How is that going to work?

crypto/ecdsa doesn’t seem to attempt to do this at all.

(Yeah, it’s hard. That’s why the previous implementation only allowed signing via a separate third-party non-Go process.)


// newCosignSignature returns a cosignSignature object with
// the specified primary contents and appropriate metadata.
func NewCosignSignature(dockerManifestDigest digest.Digest, dockerReference string) cosignSignature {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I’d prefer the public API to be higher-level, see elsewhere.

return s.SignMessage(bytes.NewReader(payload), options.WithContext(s.ctx))
}

func (s *sigstoreSigningMechanism) Upload(signature, payload []byte) (*models.LogEntryAnon, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn’t use a context.Context for timeouts/cancellation?

return nil, nil, err
}

tok, err := oauthflow.OIDConnect(oidcIssuer, oidcClientID, "", oauthflow.DefaultIDTokenGetter)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn’t use a context.Context for timeouts/cancellation?

Copy link
Author

Choose a reason for hiding this comment

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

Not currently.

},
)

resp, err := ops.SigningCert(params, bearerAuth)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn’t use a context.Context for timeouts/cancellation?

Copy link
Author

Choose a reason for hiding this comment

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

It does not today.

Copy link
Member

@vrothberg vrothberg left a comment

Choose a reason for hiding this comment

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

Thanks!

By no means a full review (I ran out of time for today).

copy/copy.go Outdated
@@ -751,6 +752,14 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
sigs = append(sigs, newSig)
}

if options.Sign {
Copy link
Member

Choose a reason for hiding this comment

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

Should Sign and SignBy conflict?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it makes enough sense to publish an image and sign it using both systems, to allow both kinds of consumers to consume it. Eventually we’ll probably recommend one of them, but that’s not a reason to prohibit signing using both.

Comment on lines 46 to 50
GenerateCertificate() error
// Sign creates a (non-detached) signature of input using ephemeral keys.
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
Sign(payload []byte) (signature []byte, err error)
Upload(signature, payload []byte) (*models.LogEntryAnon, error)
Copy link
Member

Choose a reason for hiding this comment

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

I had the same question. I think we could move it to ./internal and avoid exposing it on the API.

copy/sign.go Outdated
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports"
"github.com/pkg/errors"
)

// createSignature creates a new signature of manifest using keyIdentity.
func (c *copier) createSignature(manifest []byte, keyIdentity string) ([]byte, error) {
if keyIdentity == "" {
Copy link
Member

Choose a reason for hiding this comment

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

I'd echo that. Would make callers easier to read (like prose).

copy/sign.go Outdated
}
defer mech.Close()
if err := mech.SupportsSigning(); err != nil {
return nil, errors.Wrap(err, "signing is not supported")
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add "cosign" to the error message to leave some explicit breadcrumb (not sure whether that's set in the error).

Copy link
Member

Choose a reason for hiding this comment

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

As @mtrmac mentioned in another comment: let's use fmt instead of the errors package to slowly migrate over to the standard lib.

copy/sign.go Outdated
return nil, errors.Wrap(err, "getting key from Fulcio")
}

fmt.Println("Marshalling payload into JSON")
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 some/most of them should be logrus.Debug. I wouldn't understand what "Marshalling payload into JSON" would mean when seeing that during podman pull ... so many payloads and too much JSON everywhere :^)

copy/sign.go Outdated
return nil, err
}

fmt.Println("Signing payload")
Copy link
Member

Choose a reason for hiding this comment

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

This would be helpful on report writer but maybe "Signing image via cosign"?

Copy link
Author

Choose a reason for hiding this comment

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

Ack. Mostly just debug stuff for now. I'll need to make a pass at some point when the code is in a more stable state to make sure all user visible messages are coherent and clear.


fmt.Println("Sending entry to transparency log")
tlogEntry, err := mech.Upload(signature, sigPayload)

Copy link
Member

Choose a reason for hiding this comment

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

Lint nit: blank line

if err != nil {
return errors.Wrap(err, "marshaling")
}
canonicalized, err := jsoncanonicalizer.Transform(contents)
Copy link
Member

Choose a reason for hiding this comment

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

I'd love avoid new dependencies if possible. This one seems to be maintained by a single person which is something we should consider. In case we need to canonicalize, maybe we can use another library; ideally one we already use.

}
re := rekorEntry(payload, signature, s.cert)
returnVal := models.Rekord{
APIVersion: swag.String(re.APIVersion()),
Copy link
Member

Choose a reason for hiding this comment

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

swag.String seems to only return a pointer to the input string. Could we do this manually and get rid of adding swag as a new dependency.

Note that I am trying to avoid new dependencies unless urgently necessary to keep our footprint small; it'll add some extra work downstream packaging as well (e.g., Debian has to devendor all dependencies and put them into extra source packages).

Copy link
Author

Choose a reason for hiding this comment

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

Yes, good catch.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, https://github.com/sigstore/rekor/blob/main/pkg/generated/models/rekord.go depends on swag anyway, so I don’t think this would buy us anything.

Copy link
Author

@font font left a comment

Choose a reason for hiding this comment

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

@mtrmac @vrothberg Thank you for your early review! There is still lots of work to do and there are many areas that need to be scrubbed. But I think we need to discuss a few things before I can continue moving forward:

  1. The naming convention we want to use i.e. sigstore vs cosign, to avoid confusion and conflicts with existing naming scheme. This will likely impact user visible help strings and manpages for correct usage.
  2. UX and semantics for --sign vs --sign-by. Originally I thought maybe --sign-by being nil could indicate ephemeral key (aka "keyless') signing instead of having to introduce a new flag. But that could be confusing. In any case, we should settle on the right flag and options to consider for future-proofing this in case we want to add bring-your-own-keys, KMS, etc. type of workflows in the future.
  3. The right level of abstraction for introducing this feature's APIs and how to separate it with existing simple signing approach.

copy/sign.go Outdated
return nil, errors.Wrap(err, "getting key from Fulcio")
}

fmt.Println("Marshalling payload into JSON")
Copy link
Author

Choose a reason for hiding this comment

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

Ack, mostly just debug stuff for now.

copy/copy.go Outdated
@@ -123,6 +123,7 @@ type ImageListSelection int
// Options allows supplying non-default configuration modifying the behavior of CopyImage.
type Options struct {
RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature.
Sign bool // Adds cosign signature using ephemeral keys.
Copy link
Author

Choose a reason for hiding this comment

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

Yes, I think we need to discuss what are all possible Cosign signing options we can foresee adding in the future. As a result, I think it makes sense to expand this to a struct.

copy/sign.go Outdated
return nil, err
}

fmt.Println("Signing payload")
Copy link
Author

Choose a reason for hiding this comment

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

Ack. Mostly just debug stuff for now. I'll need to make a pass at some point when the code is in a more stable state to make sure all user visible messages are coherent and clear.

// the usage of the BaseURL is defined under docker/distribution registries—separate storage of docs/signature-protocols.md
// Warning: This function only exposes configuration in registries.d;
// just because this function returns an URL does not mean that the URL will be used by c/image/docker (e.g. if the registry natively supports X-R-S-S).
func CosignStorageBaseURL(sys *types.SystemContext, ref types.ImageReference, write bool) (*url.URL, error) {
Copy link
Author

Choose a reason for hiding this comment

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

Yes, it's mostly duplicated, with the underlying paths differing between cosign and the original simple signing look aside location. I was trying an expedient way to differentiate between the two formats in the look aside, and thought having a separate path may be the easiest for now since there's no policy configuration for cosign yet. I'll need to think through and discuss this with you further.

Comment on lines 46 to 50
GenerateCertificate() error
// Sign creates a (non-detached) signature of input using ephemeral keys.
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
Sign(payload []byte) (signature []byte, err error)
Upload(signature, payload []byte) (*models.LogEntryAnon, error)
Copy link
Author

Choose a reason for hiding this comment

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

Ack, I was trying to leverage the existing APIs that existed for SigningMechanism until I had a better understanding of what all would be needed for this new signing method. I expect this to be further refined, and you say, likely not even made public depending on the requirements.

return nil, nil, err
}

tok, err := oauthflow.OIDConnect(oidcIssuer, oidcClientID, "", oauthflow.DefaultIDTokenGetter)
Copy link
Author

Choose a reason for hiding this comment

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

Not currently.

},
)

resp, err := ops.SigningCert(params, bearerAuth)
Copy link
Author

Choose a reason for hiding this comment

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

It does not today.

}
re := rekorEntry(payload, signature, s.cert)
returnVal := models.Rekord{
APIVersion: swag.String(re.APIVersion()),
Copy link
Author

Choose a reason for hiding this comment

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

Yes, good catch.

// Here, we display the proof and succeed.
if existsErr, ok := err.(*entries.CreateLogEntryConflict); ok {

fmt.Println("Signature already exists. Displaying proof")
Copy link
Author

Choose a reason for hiding this comment

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

Sure, we can just succeed silently, or display that the entry already exists. That will simplify things a bit.

Comment on lines 213 to 214
// Use intermediate variables for these values so that we can take their addresses.
// Golang guarantees that they will have a new address on every execution.
Copy link
Author

Choose a reason for hiding this comment

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

Apologies, this code is very much WIP and still needs to be scrubbed of things like this.

Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
This adds a new ImageDestinationSigstore private interface with a
SupportsSigstoreSignatures method for detecting whether the destination
transport i.e. docker only, supports Sigstore signatures.

Signed-off-by: Ivan Font <ifont@redhat.com>
Make functions and methods that are not invoked outside package into
private unexported functions and methods.

Signed-off-by: Ivan Font <ifont@redhat.com>
Signed-off-by: Ivan Font <ifont@redhat.com>
This adds new Sigstore APIs for signing docker manifests using the
cosign signing spec and uploading the signature and payload of the
result to the Sigstore transparency log.

Signed-off-by: Ivan Font <ifont@redhat.com>
@font
Copy link
Author

font commented Oct 2, 2021

Still isn't ready for review necessarily, but I've pushed another update to start addressing some of the feedback.

@mtrmac
Copy link
Collaborator

mtrmac commented Oct 4, 2021

@font I realize you are pressed for time; the part that would help us most most of all to see how c/image needs to change would be to see a verification implementation (what are the from-registry inputs, what are the local-client trust configurations and roots, and how do they combine into a yes/no decision).

@mtrmac
Copy link
Collaborator

mtrmac commented Jan 12, 2023

#1785 , and pre-requisite PRs, is now a doing everything from this PR that we want to do (an example exception: don’t care to cryptographically validate the Rekor-returned data against that Rekor’s public key).

Thanks for helping with this!

@mtrmac mtrmac closed this Jan 12, 2023
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.

None yet

3 participants