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

Support mTLS towards container registry #3922

Merged
merged 5 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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
}
220 changes: 220 additions & 0 deletions cmd/cosign/cli/options/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// 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, isCa bool) (certficateLocation, privateKeyLocation string) {
certficateLocation = createTempFile(t)
privateKeyLocation = createTempFile(t)

// 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) {
validCaCertificate, validCaKey := generateCertificate(t, true)
validClientCertificate, validClientKey := generateCertificate(t, false)

t.Cleanup(func() {
removeTempFile(t, validCaCertificate)
removeTempFile(t, validCaKey)
removeTempFile(t, validClientCertificate)
removeTempFile(t, validClientKey)
})

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) string {
tmpfile, err := os.CreateTemp("", "registry-test-")
if err != nil {
t.Fatal(err)
}

return tmpfile.Name()
}

// Helper function to remove temporary files after testing
func removeTempFile(t *testing.T, filename string) {
if err := os.Remove(filename); err != nil {
t.Fatal(err)
}
}
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
Loading