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

feat: adds cosign support #341

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions commands/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"github.com/simonshyu/notary-gcr/pkg/gcr"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -223,6 +226,15 @@ func put(req resource.OutRequest, img partial.WithRawManifest, tags []name.Tag,

logrus.Info("pushed")

if req.Source.Cosign != nil {
switch t := img.(type) {
case v1.Image:
signImagesCosign(req, t, tags)
default:
return fmt.Errorf("cannot sign type (%T)", img)
}
}

if req.Source.ContentTrust != nil {
switch t := img.(type) {
case v1.Image:
Expand All @@ -238,6 +250,160 @@ func put(req resource.OutRequest, img partial.WithRawManifest, tags []name.Tag,
return nil
}

// struct to hold username and password
type cosignUserCredentials struct {
username string
password string
}

// A keychain that is able to return the creds based on the registry url
type keyChain struct {
// An in-memory map of username/passwords
credentials map[string]cosignUserCredentials
}

func (k *keyChain) Resolve(resource authn.Resource) (authn.Authenticator, error) {

registryHost := resource.RegistryStr()

userCreds, ok := k.credentials[registryHost]

if !ok {
return authn.Anonymous, fmt.Errorf("unable to find credentials for host %s", registryHost)
}

return authn.FromConfig(authn.AuthConfig{
Username: userCreds.username,
Password: userCreds.password,
}), nil
}

func signImagesCosign(req resource.OutRequest, img v1.Image, tags []name.Tag) error {
digest, err := img.Digest()
if err != nil {
return fmt.Errorf("error getting digest for image: %v", err)
}

// here we build the img digest url so that we can sign it with cosign.
// we sign the digest URL and not just the tag URL because tags can point
// to difference digests over time. whereas a digest is supposed to represent
// and specific image build and won't change (although technically Docker cannot produce
// image digests that stay the same all the time, this is where tools like `apko`
// are a great tool for building OCI images)
imgDigestUrl := tags[0].String() + "@" + digest.String()

// over time this won't be needed. the Cosign library will evolve to expect
// less object parameters to be configured. this is only needed for now due to
// the fact that primarily cosign is a cli tool. And part of the CLI framework
// that cosign uses, sets the majority of the below objects variables by default.
// this comment concerns the below `rootOptions`, `signOpts` and `keyOpts` data objects.
rootOptions := &options.RootOptions{
OutputFile: "",
Verbose: false,
Timeout: options.DefaultTimeout,
}

keychain := &keyChain{
credentials: map[string]cosignUserCredentials{
req.Source.Cosign.Registry: {
username: req.Source.Username,
password: req.Source.Password,
},
},
}

signOpts := options.SignOptions{
Key: "env://COSIGN_KEY",
Cert: "",
CertChain: "",
Upload: true,
OutputSignature: "",
OutputPayload: "",
OutputCertificate: "",
PayloadPath: "",
Recursive: false,
Attachment: "",
SkipConfirmation: true,
TlogUpload: false,
TSAServerURL: "",
IssueCertificate: false,
Fulcio: options.FulcioOptions{
URL: options.DefaultFulcioURL,
},
OIDC: options.OIDCOptions{
Issuer: options.DefaultOIDCIssuerURL,
ClientID: "sigstore",
RedirectURL: "",
},
Rekor: options.RekorOptions{
URL: options.DefaultRekorURL,
},
Registry: options.RegistryOptions{
AllowInsecure: false,
KubernetesKeychain: false,
Keychain: keychain,
},
}

keyOpts := options.KeyOpts{
KeyRef: "env://COSIGN_KEY",
PassFunc: generate.GetPass,
Sk: false,
Slot: "",
FulcioURL: options.DefaultFulcioURL,
IDToken: "",
InsecureSkipFulcioVerify: false,
RekorURL: options.DefaultRekorURL,
OIDCIssuer: options.DefaultOIDCIssuerURL,
OIDCClientID: "sigstore",
OIDCClientSecret: "",
OIDCRedirectURL: "",
OIDCDisableProviders: false,
OIDCProvider: "",
SkipConfirmation: true,
TSAServerURL: "",
IssueCertificateForExistingKey: false,
}

// because cosign is a CLI tool primiarly (until it has matured as a library)
// we have to set the COSIGN_KEY environment variable and tell cosign to use it.
// as it is less tricky than having to create files that contain the cosign.key
// which actually becomes less secure because anyone who gains access to the container
// can easily easily see the file. whereas the `os.Setenv` function does not permanently
// set environment variables for the parent process that runs it, only for child processes.
// however, it is still not perfect, we would rather pass the key directly to cosign but
// untill that functionality is offered this is the best option without making decisions on
// tooling (Vault, Azure KeyVault etc)
if req.Source.Cosign.Key == "" {
return fmt.Errorf("Cosign.Key cannot be empty")
}
err = os.Setenv("COSIGN_KEY", req.Source.Cosign.Key)
if err != nil {
return fmt.Errorf("err %w", err)
}

// similiar to the COSIGN_KEY variable we set the password that was used to create the
// keypair (if there was one). there are less ways to set the password currently outside
// of an environment variable or user input via a terminal prompt, as the Cosign library
// evolves over time, we can reasonably expect this to change, but as we cannot rely on user
// input, we have to use the environment variable.
if req.Source.Cosign.Password == "" {
return fmt.Errorf("Cosign.Password cannot be empty")
}
err = os.Setenv("COSIGN_PASSWORD", req.Source.Cosign.Password)
if err != nil {
return fmt.Errorf("err %w", err)
}

logrus.Infof("Signing image with Cosign: %s", imgDigestUrl)
err = sign.SignCmd(rootOptions, keyOpts, signOpts, []string{imgDigestUrl})
if err != nil {
return fmt.Errorf("there was an error signing the image with Cosign: %w", err)
}
logrus.Infof("Image signed with Cosign")
return nil
}

func loadImage(path string) (partial.WithRawManifest, error) {
stat, err := os.Stat(path)
if err != nil {
Expand Down
Loading