From e9ebc2523bac5388bd3a9dc2fed67bc39fb2b892 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Wed, 3 May 2023 19:39:20 +0200 Subject: [PATCH] add 'cosign sign' command-line parameters for mTLS Add command-line parameters for key/cert/cacert used for the connection to the TSA server. Fixes #3006 Signed-off-by: Dmitry S --- cmd/cosign/cli/options/key.go | 4 + cmd/cosign/cli/options/sign.go | 18 ++++ cmd/cosign/cli/sign.go | 4 + cmd/cosign/cli/sign/sign.go | 11 ++- doc/cosign_sign.md | 4 + internal/pkg/cosign/tsa/client/client.go | 112 ++++++++++++++++++++++- 6 files changed, 151 insertions(+), 2 deletions(-) diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go index 1100ca083d18..6af69afda374 100644 --- a/cmd/cosign/cli/options/key.go +++ b/cmd/cosign/cli/options/key.go @@ -33,6 +33,10 @@ type KeyOpts struct { OIDCProvider string // Specify which OIDC credential provider to use for keyless signer BundlePath string SkipConfirmation bool + TSAClientCACert string + TSAClientCert string + TSAClientKey string + TSAServerName string // expected SAN field in the TSA server's certificate - https://pkg.go.dev/crypto/tls#Config.ServerName TSAServerURL string RFC3161TimestampPath string TSACertChainPath string diff --git a/cmd/cosign/cli/options/sign.go b/cmd/cosign/cli/options/sign.go index 0359fa8e6e94..aeac9cad6e08 100644 --- a/cmd/cosign/cli/options/sign.go +++ b/cmd/cosign/cli/options/sign.go @@ -34,6 +34,10 @@ type SignOptions struct { Attachment string SkipConfirmation bool TlogUpload bool + TSAClientCACert string + TSAClientCert string + TSAClientKey string + TSAServerName string TSAServerURL string IssueCertificate bool SignContainerIdentity string @@ -104,9 +108,23 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", true, "whether or not to upload to the tlog") + cmd.Flags().StringVar(&o.TSAClientCACert, "timestamp-client-cacert", "", + "path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server") + + cmd.Flags().StringVar(&o.TSAClientCert, "timestamp-client-cert", "", + "path to the X.509 certificate file in PEM format to be used for the connection to the TSA Server") + + cmd.Flags().StringVar(&o.TSAClientKey, "timestamp-client-key", "", + "path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server") + + cmd.Flags().StringVar(&o.TSAServerName, "timestamp-server-name", "", + "SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server") + cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", "url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr") + _ = cmd.Flags().SetAnnotation("certificate", cobra.BashCompFilenameExt, []string{"cert"}) + cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, "issue a code signing certificate from Fulcio, even if a key is provided") diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 78d513813254..a276bccf4247 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -113,6 +113,10 @@ race conditions or (worse) malicious tampering. OIDCDisableProviders: o.OIDC.DisableAmbientProviders, OIDCProvider: o.OIDC.Provider, SkipConfirmation: o.SkipConfirmation, + TSAClientCACert: o.TSAClientCACert, + TSAClientCert: o.TSAClientCert, + TSAClientKey: o.TSAClientKey, + TSAServerName: o.TSAServerName, TSAServerURL: o.TSAServerURL, IssueCertificateForExistingKey: o.IssueCertificate, } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index eaea6d72e038..ba286981d24c 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -239,7 +239,16 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti } if ko.TSAServerURL != "" { - s = tsa.NewSigner(s, client.NewTSAClient(ko.TSAServerURL)) + if ko.TSAClientCACert == "" && ko.TSAClientCert == "" { // no mTLS params or custom CA + s = tsa.NewSigner(s, client.NewTSAClient(ko.TSAServerURL)) + } else { + s = tsa.NewSigner(s, client.NewTSAClientMTLS(ko.TSAServerURL, + ko.TSAClientCACert, + ko.TSAClientCert, + ko.TSAClientKey, + ko.TSAServerName, + )) + } } shouldUpload, err := ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) if err != nil { diff --git a/doc/cosign_sign.md b/doc/cosign_sign.md index be48ff796c33..ea62bc782e6d 100644 --- a/doc/cosign_sign.md +++ b/doc/cosign_sign.md @@ -99,6 +99,10 @@ cosign sign [flags] --sign-container-identity string manually set the .critical.docker-reference field for the signed identity, which is useful when image proxies are being used where the pull reference should match the signature --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) + --timestamp-client-cacert string path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server + --timestamp-client-cert string path to the X.509 certificate file in PEM format to be used for the connection to the TSA Server + --timestamp-client-key string path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server + --timestamp-server-name string SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server --timestamp-server-url string url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr --tlog-upload whether or not to upload to the tlog (default true) --upload whether to upload the signature (default true) diff --git a/internal/pkg/cosign/tsa/client/client.go b/internal/pkg/cosign/tsa/client/client.go index 380f270bf540..b891b33e062a 100644 --- a/internal/pkg/cosign/tsa/client/client.go +++ b/internal/pkg/cosign/tsa/client/client.go @@ -16,8 +16,11 @@ package client import ( "bytes" + "crypto/tls" + "crypto/x509" "fmt" "io" + "net" "net/http" "os" "time" @@ -37,17 +40,113 @@ type TimestampAuthorityClientImpl struct { // URL is the path to the API to request timestamp responses URL string + // CACert is the filepath to the PEM-encoded CA certificate for the connection to the TSA server + CACert string + // Cert is the filepath to the PEM-encoded certificate for the connection to the TSA server + Cert string + // Cert is the filepath to the PEM-encoded key corresponding to the certificate for the connection to the TSA server + Key string + // ServerName is the expected SAN value in the server's certificate - used for https://pkg.go.dev/crypto/tls#Config.ServerName + ServerName string // Timeout is the request timeout Timeout time.Duration } +const defaultTimeout = 10 * time.Second + +func getHTTPTransport(cacertFilename, certFilename, keyFilename, serverName string, timeout time.Duration) (http.RoundTripper, error) { + if timeout == 0 { + timeout = defaultTimeout + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + CipherSuites: []uint16{ + // TLS 1.3 cipher suites. + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + }, + MinVersion: tls.VersionTLS13, + SessionTicketsDisabled: true, + }, + // the rest of default settings are copied verbatim from https://golang.org/pkg/net/http/#DefaultTransport + // to minimize surprises for the users. + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + var pool *x509.CertPool + if cacertFilename != "" { + f, err := os.Open(cacertFilename) + if err != nil { + return nil, err + } + defer f.Close() + caCertBytes, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("unable to read CA certs from %s: %w", cacertFilename, err) + } + pool = x509.NewCertPool() + if !pool.AppendCertsFromPEM(caCertBytes) { + return nil, fmt.Errorf("no valid CA certs found in %s", cacertFilename) + } + tr.TLSClientConfig.RootCAs = pool + } + if certFilename != "" && keyFilename != "" { + cert, err := tls.LoadX509KeyPair(certFilename, keyFilename) + if err != nil { + return nil, fmt.Errorf("unable to read CA certs from cert %s, key %s: %w", + certFilename, keyFilename, err) + } + tr.TLSClientConfig.Certificates = []tls.Certificate{cert} + } + + if serverName != "" { + // copied from https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection, + // changed the DNSName setting from cs.Servername to serverName + tr.TLSClientConfig.InsecureSkipVerify = true + tr.TLSClientConfig.VerifyConnection = func(cs tls.ConnectionState) error { + opts := x509.VerifyOptions{ + DNSName: serverName, + Intermediates: x509.NewCertPool(), + Roots: pool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + } + for _, cert := range cs.PeerCertificates[1:] { + opts.Intermediates.AddCert(cert) + } + + _, err := cs.PeerCertificates[0].Verify(opts) + return err + } + } + return tr, nil +} + // GetTimestampResponse sends a timestamp query to a timestamp authority, returning a timestamp response. // The query and response are defined by RFC 3161. func (t *TimestampAuthorityClientImpl) GetTimestampResponse(tsq []byte) ([]byte, error) { client := http.Client{ Timeout: t.Timeout, } + + // if mTLS-related fields are set, create a custom Transport for the Client + if t.CACert != "" || t.Cert != "" { + tr, err := getHTTPTransport(t.CACert, t.Cert, t.Key, t.ServerName, t.Timeout) + if err != nil { + return nil, err + } + client.Transport = tr + } + req, err := http.NewRequest("POST", t.URL, bytes.NewReader(tsq)) if err != nil { return nil, errors.Wrap(err, "error creating HTTP request") @@ -79,5 +178,16 @@ func (t *TimestampAuthorityClientImpl) GetTimestampResponse(tsq []byte) ([]byte, } func NewTSAClient(url string) *TimestampAuthorityClientImpl { - return &TimestampAuthorityClientImpl{URL: url, Timeout: 10 * time.Second} + return &TimestampAuthorityClientImpl{URL: url, Timeout: defaultTimeout} +} + +func NewTSAClientMTLS(url, cacert, cert, key, serverName string) *TimestampAuthorityClientImpl { + return &TimestampAuthorityClientImpl{ + URL: url, + CACert: cacert, + Cert: cert, + Key: key, + ServerName: serverName, + Timeout: defaultTimeout, + } }