-
Notifications
You must be signed in to change notification settings - Fork 362
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
Conversation
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 == "" { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :^)
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 bySign
? - 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.
docker/lookaside.go
Outdated
// 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) { |
There was a problem hiding this comment.
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.]
There was a problem hiding this comment.
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.
signature/mechanism_sigstore.go
Outdated
|
||
// TODO: use debug log | ||
fmt.Println("Generating ephemeral keys") | ||
priv, err := generatePrivateKey() |
There was a problem hiding this comment.
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.)
signature/signature.go
Outdated
|
||
// newCosignSignature returns a cosignSignature object with | ||
// the specified primary contents and appropriate metadata. | ||
func NewCosignSignature(dockerManifestDigest digest.Digest, dockerReference string) cosignSignature { |
There was a problem hiding this comment.
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.
signature/mechanism_sigstore.go
Outdated
return s.SignMessage(bytes.NewReader(payload), options.WithContext(s.ctx)) | ||
} | ||
|
||
func (s *sigstoreSigningMechanism) Upload(signature, payload []byte) (*models.LogEntryAnon, error) { |
There was a problem hiding this comment.
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?
signature/mechanism_sigstore.go
Outdated
return nil, nil, err | ||
} | ||
|
||
tok, err := oauthflow.OIDConnect(oidcIssuer, oidcClientID, "", oauthflow.DefaultIDTokenGetter) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not currently.
signature/mechanism_sigstore.go
Outdated
}, | ||
) | ||
|
||
resp, err := ops.SigningCert(params, bearerAuth) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not today.
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
signature/mechanism.go
Outdated
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) |
There was a problem hiding this comment.
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 == "" { |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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"
?
There was a problem hiding this comment.
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lint nit: blank line
signature/mechanism_sigstore.go
Outdated
if err != nil { | ||
return errors.Wrap(err, "marshaling") | ||
} | ||
canonicalized, err := jsoncanonicalizer.Transform(contents) |
There was a problem hiding this comment.
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.
signature/mechanism_sigstore.go
Outdated
} | ||
re := rekorEntry(payload, signature, s.cert) | ||
returnVal := models.Rekord{ | ||
APIVersion: swag.String(re.APIVersion()), |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch.
There was a problem hiding this comment.
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.
There was a problem hiding this 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:
- 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.
- UX and semantics for
--sign
vs--sign-by
. Originally I thought maybe--sign-by
beingnil
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. - 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") |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
docker/lookaside.go
Outdated
// 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) { |
There was a problem hiding this comment.
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.
signature/mechanism.go
Outdated
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) |
There was a problem hiding this comment.
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.
signature/mechanism_sigstore.go
Outdated
return nil, nil, err | ||
} | ||
|
||
tok, err := oauthflow.OIDConnect(oidcIssuer, oidcClientID, "", oauthflow.DefaultIDTokenGetter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not currently.
signature/mechanism_sigstore.go
Outdated
}, | ||
) | ||
|
||
resp, err := ops.SigningCert(params, bearerAuth) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not today.
signature/mechanism_sigstore.go
Outdated
} | ||
re := rekorEntry(payload, signature, s.cert) | ||
returnVal := models.Rekord{ | ||
APIVersion: swag.String(re.APIVersion()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch.
signature/mechanism_sigstore.go
Outdated
// Here, we display the proof and succeed. | ||
if existsErr, ok := err.(*entries.CreateLogEntryConflict); ok { | ||
|
||
fmt.Println("Signature already exists. Displaying proof") |
There was a problem hiding this comment.
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.
signature/signature.go
Outdated
// 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. |
There was a problem hiding this comment.
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>
Still isn't ready for review necessarily, but I've pushed another update to start addressing some of the feedback. |
@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). |
#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! |
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:
http://localhost:5556/auth/callback
is not successfully launched. Need to continue investigating this issue.putSignature
) to the registry.copyMultipleImages
.registries.d/default.yaml
field name and location for storing Sigstore's cosign signatures.Optional
simple signing field.