Skip to content

Commit

Permalink
Support mTLS towards container registry (#3922)
Browse files Browse the repository at this point in the history
* Support for client certs towards registry server

This commit refactors the registry options handling in the
`cmd/cosign/cli/options/registry.go` file. It introduces new flags for
specifying the X.509 CA certificate, client certificate, client key, and
server name for the connection to the registry.
This allows cosign to connect to registries that requires mTLS for
authentication.

Signed-off-by: Søren Juul <[email protected]>

* Update documentation

Signed-off-by: Søren Juul <[email protected]>

* Add registry_test.go

Increase test coverage of `getTLSConfig` method.

Signed-off-by: Søren Juul <[email protected]>

* Fix unittests on win and linter errors

Signed-off-by: Søren Juul <[email protected]>

* Fix temp file creation

Signed-off-by: Søren Juul <[email protected]>

---------

Signed-off-by: Søren Juul <[email protected]>
  • Loading branch information
zpon authored Nov 6, 2024
1 parent 38c8a28 commit ad5bc3b
Show file tree
Hide file tree
Showing 23 changed files with 354 additions and 2 deletions.
61 changes: 59 additions & 2 deletions cmd/cosign/cli/options/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ package options
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"net/http"
"os"

ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
"github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
Expand All @@ -45,6 +47,10 @@ type RegistryOptions struct {
RefOpts ReferenceOptions
Keychain Keychain
AuthConfig authn.AuthConfig
RegistryCACert string
RegistryClientCert string
RegistryClientKey string
RegistryServerName string

// RegistryClientOpts allows overriding the result of GetRegistryClientOpts.
RegistryClientOpts []remote.Option
Expand Down Expand Up @@ -72,6 +78,18 @@ func (o *RegistryOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.AuthConfig.RegistryToken, "registry-token", "",
"registry bearer auth token")

cmd.Flags().StringVar(&o.RegistryCACert, "registry-cacert", "",
"path to the X.509 CA certificate file in PEM format to be used for the connection to the registry")

cmd.Flags().StringVar(&o.RegistryClientCert, "registry-client-cert", "",
"path to the X.509 certificate file in PEM format to be used for the connection to the registry")

cmd.Flags().StringVar(&o.RegistryClientKey, "registry-client-key", "",
"path to the X.509 private key file in PEM format to be used, together with the 'registry-client-cert' value, for the connection to the registry")

cmd.Flags().StringVar(&o.RegistryServerName, "registry-server-name", "",
"SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the registry")

o.RefOpts.AddFlags(cmd)
}

Expand Down Expand Up @@ -131,8 +149,9 @@ func (o *RegistryOptions) GetRegistryClientOpts(ctx context.Context) []remote.Op
opts = append(opts, remote.WithAuthFromKeychain(authn.DefaultKeychain))
}

if o.AllowInsecure {
opts = append(opts, remote.WithTransport(&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}})) // #nosec G402
tlsConfig, err := o.getTLSConfig()
if err == nil {
opts = append(opts, remote.WithTransport(&http.Transport{TLSClientConfig: tlsConfig}))
}

// Reuse a remote.Pusher and a remote.Puller for all operations that use these opts.
Expand Down Expand Up @@ -193,3 +212,41 @@ func (o *RegistryExperimentalOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().Var(&o.RegistryReferrersMode, "registry-referrers-mode",
"mode for fetching references from the registry. allowed: legacy, oci-1-1")
}

func (o *RegistryOptions) getTLSConfig() (*tls.Config, error) {
var tlsConfig tls.Config

if o.RegistryCACert != "" {
f, err := os.Open(o.RegistryCACert)
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", o.RegistryCACert, err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCertBytes) {
return nil, fmt.Errorf("no valid CA certs found in %s", o.RegistryCACert)
}
tlsConfig.RootCAs = pool
}

if o.RegistryClientCert != "" && o.RegistryClientKey != "" {
cert, err := tls.LoadX509KeyPair(o.RegistryClientCert, o.RegistryClientKey)
if err != nil {
return nil, fmt.Errorf("unable to read client certs from cert %s, key %s: %w",
o.RegistryClientCert, o.RegistryClientKey, err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}

if o.RegistryServerName != "" {
tlsConfig.ServerName = o.RegistryServerName
}

tlsConfig.InsecureSkipVerify = o.AllowInsecure // #nosec G402

return &tlsConfig, nil
}
211 changes: 211 additions & 0 deletions cmd/cosign/cli/options/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright 2021 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package options

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"strings"
"testing"
"time"
)

func generatePrivateKey(t *testing.T) *rsa.PrivateKey {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}

return privateKey
}

func writePrivateKey(t *testing.T, privateKey *rsa.PrivateKey, fileLocation string) {
// Encode the private key to PEM format
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})

// Write the private key to the specified file
err := os.WriteFile(fileLocation, privateKeyPEM, 0600)
if err != nil {
t.Fatal(err)
}
}

func generateCertificate(t *testing.T, dir string, isCa bool) (certficateLocation, privateKeyLocation string) {
certficateLocation = createTempFile(t, dir)
privateKeyLocation = createTempFile(t, dir)

// Generate a private key for the CA
privateKey := generatePrivateKey(t)

// Create a self-signed certificate for the CA
caTemplate := &x509.Certificate{
Subject: pkix.Name{
CommonName: "Test CA",
},
NotBefore: time.Now().Add(time.Hour * -24),
NotAfter: time.Now().Add(time.Hour * 24),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: isCa,
SerialNumber: big.NewInt(1337),
}

caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &privateKey.PublicKey, privateKey)
if err != nil {
t.Fatal(err)
}

// Encode the CA certificate to PEM format
caCertPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: caCertDER,
})

// Write the CA certificate to the specified file
err = os.WriteFile(certficateLocation, caCertPEM, 0644)
if err != nil {
t.Fatal(err)
}

writePrivateKey(t, privateKey, privateKeyLocation)

return certficateLocation, privateKeyLocation
}

func TestGetTLSConfig(t *testing.T) {
tempDir := t.TempDir() // Create a temporary directory for testing
t.Cleanup(func() {
os.RemoveAll(tempDir)
})
validCaCertificate, validCaKey := generateCertificate(t, tempDir, true)
validClientCertificate, validClientKey := generateCertificate(t, tempDir, false)

tests := []struct {
name string
registryCACert string
registryClientCert string
registryClientKey string
registryServerName string
allowInsecure bool
expectError string
}{
{
name: "Valid CA Cert, Client Cert and Key, Server Name, Allow Insecure",
registryCACert: validCaCertificate,
registryClientCert: validClientCertificate,
registryClientKey: validClientKey,
registryServerName: "example.com",
allowInsecure: true,
},
{
name: "Wrong key for client cert",
registryCACert: validCaCertificate,
registryClientCert: validClientCertificate,
registryClientKey: validCaKey, // using ca key for client cert must fail
registryServerName: "example.com",
allowInsecure: true,
expectError: fmt.Sprintf("unable to read client certs from cert %s, key %s: tls: private key does not match public key", validClientCertificate, validCaKey),
},
{
name: "Wrong ca key",
registryCACert: validClientKey, // using client key for ca cert must fail
registryClientCert: validClientCertificate,
registryClientKey: validClientKey,
registryServerName: "example.com",
allowInsecure: true,
expectError: fmt.Sprintf("no valid CA certs found in %s", validClientKey),
},
{
name: "Invalid CA path",
registryCACert: "/not/existing/path/fooobar", // this path is not expected to exist
registryClientCert: validClientCertificate,
registryClientKey: validClientKey,
registryServerName: "example.com",
allowInsecure: true,
expectError: "open /not/existing/path/fooobar: ", // the error message is OS dependent
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &RegistryOptions{
RegistryCACert: tt.registryCACert,
RegistryClientCert: tt.registryClientCert,
RegistryClientKey: tt.registryClientKey,
RegistryServerName: tt.registryServerName,
AllowInsecure: tt.allowInsecure,
}

tlsConfig, err := o.getTLSConfig()
if tt.expectError != "" {
if err == nil || !strings.HasPrefix(err.Error(), tt.expectError) {
t.Errorf("getTLSConfig()\nerror: \"%v\",\nexpectError: \"%v\"", err, tt.expectError)
return
}
} else {
if err != nil {
t.Errorf("getTLSConfig() error = %v, expectError %v", err, tt.expectError)
return
}
}

if err == nil {
if tt.registryCACert != "" {
if tlsConfig.RootCAs == nil {
t.Errorf("Expected RootCAs to be set")
}
}

if tt.registryClientCert != "" && tt.registryClientKey != "" {
if len(tlsConfig.Certificates) == 0 {
t.Errorf("Expected Certificates to be set")
}
}

if tt.registryServerName != "" {
if tlsConfig.ServerName != tt.registryServerName {
t.Errorf("Expected ServerName to be %s, got %s", tt.registryServerName, tlsConfig.ServerName)
}
}

if tt.allowInsecure {
if !tlsConfig.InsecureSkipVerify {
t.Errorf("Expected InsecureSkipVerify to be true")
}
}
}
})
}
}

// Helper function to create temporary files for testing
func createTempFile(t *testing.T, dir string) string {
tmpfile, err := os.CreateTemp(dir, "registry-test-")
if err != nil {
t.Fatal(err)
}
defer tmpfile.Close()

return tmpfile.Name()
}
4 changes: 4 additions & 0 deletions doc/cosign_attach_attestation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions doc/cosign_attach_sbom.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions doc/cosign_attach_signature.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions doc/cosign_attest.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions doc/cosign_clean.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ad5bc3b

Please sign in to comment.