From 5e52fc088203d6518e3e9fd6189d2679519a33c7 Mon Sep 17 00:00:00 2001 From: Andrew Block Date: Mon, 1 Apr 2024 07:04:31 -0500 Subject: [PATCH] Added mTLS options Signed-off-by: Andrew Block --- cmd/oras/internal/option/remote.go | 18 +- cmd/oras/internal/option/remote_test.go | 272 +++++++++++++++++++++++- 2 files changed, 282 insertions(+), 8 deletions(-) diff --git a/cmd/oras/internal/option/remote.go b/cmd/oras/internal/option/remote.go index abe24ce3d..3cc24c44e 100644 --- a/cmd/oras/internal/option/remote.go +++ b/cmd/oras/internal/option/remote.go @@ -46,6 +46,9 @@ import ( ) const ( + caFileFlag = "ca-file" + certFileFlag = "cert-file" + keyFileFlag = "key-file" usernameFlag = "username" passwordFlag = "password" passwordFromStdinFlag = "password-stdin" @@ -58,6 +61,8 @@ const ( type Remote struct { DistributionSpec CACertFilePath string + CertFilePath string + KeyFilePath string Insecure bool Configs []string Username string @@ -120,7 +125,9 @@ func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description opts.plainHTTP = func() (bool, bool) { return *plainHTTP, fs.Changed(plainHTTPFlagName) } - fs.StringVar(&opts.CACertFilePath, opts.flagPrefix+"ca-file", "", "server certificate authority file for the remote "+notePrefix+"registry") + fs.StringVar(&opts.CACertFilePath, opts.flagPrefix+caFileFlag, "", "server certificate authority file for the remote "+notePrefix+"registry") + fs.StringVarP(&opts.CertFilePath, opts.flagPrefix+certFileFlag, "", "", "client certificate authority file for the remote "+notePrefix+"registry") + fs.StringVarP(&opts.KeyFilePath, opts.flagPrefix+keyFileFlag, "", "", "client private key file for the remote "+notePrefix+"registry") fs.StringArrayVar(&opts.resolveFlag, opts.flagPrefix+"resolve", nil, "customized DNS for "+notePrefix+"registry, formatted in `host:port:address[:address_port]`") fs.StringArrayVar(&opts.Configs, opts.flagPrefix+"registry-config", nil, "`path` of the authentication file for "+notePrefix+"registry") fs.StringArrayVarP(&opts.headerFlags, opts.flagPrefix+"header", shortHeader, nil, "add custom headers to "+notePrefix+"requests") @@ -142,6 +149,7 @@ func CheckStdinConflict(flags *pflag.FlagSet) error { func (opts *Remote) Parse(cmd *cobra.Command) error { usernameAndIdTokenFlags := []string{opts.flagPrefix + usernameFlag, opts.flagPrefix + identityTokenFlag} passwordAndIdTokenFlags := []string{opts.flagPrefix + passwordFlag, opts.flagPrefix + identityTokenFlag} + certFileAndKeyFileFlags := []string{opts.flagPrefix + certFileFlag, opts.flagPrefix + keyFileFlag} if cmd.Flags().Lookup(identityTokenFromStdinFlag) != nil { usernameAndIdTokenFlags = append(usernameAndIdTokenFlags, identityTokenFromStdinFlag) passwordAndIdTokenFlags = append(passwordAndIdTokenFlags, identityTokenFromStdinFlag) @@ -151,6 +159,7 @@ func (opts *Remote) Parse(cmd *cobra.Command) error { } cmd.MarkFlagsMutuallyExclusive(usernameAndIdTokenFlags...) cmd.MarkFlagsMutuallyExclusive(passwordAndIdTokenFlags...) + cmd.MarkFlagsRequiredTogether(certFileAndKeyFileFlags...) if err := opts.parseCustomHeaders(); err != nil { return err } @@ -227,6 +236,13 @@ func (opts *Remote) tlsConfig() (*tls.Config, error) { return nil, err } } + if opts.CertFilePath != "" && opts.KeyFilePath != "" { + cert, err := tls.LoadX509KeyPair(opts.CertFilePath, opts.KeyFilePath) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{cert} + } return config, nil } diff --git a/cmd/oras/internal/option/remote_test.go b/cmd/oras/internal/option/remote_test.go index bc5a071f2..573fe76d8 100644 --- a/cmd/oras/internal/option/remote_test.go +++ b/cmd/oras/internal/option/remote_test.go @@ -18,9 +18,10 @@ package option import ( "context" "crypto/rand" + "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/json" - "encoding/pem" "fmt" "net/http" "net/http/httptest" @@ -28,6 +29,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "github.com/sirupsen/logrus" @@ -43,9 +45,213 @@ var testTagList = struct { Tags: []string{"tag"}, } +// localhostServerCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// adapted from src/crypto/tls: +// go run generate_cert.go --rsa-bits 4096 --host 127.0.0.1,::1,oras.land --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostServerCert = []byte(`-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgIRAPzEFsDt4E8GxCNLsF76j18wDQYJKoZIhvcNAQELBQAw +DzENMAsGA1UEChMET1JBUzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2MDAw +MFowDzENMAsGA1UEChMET1JBUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAKcbdhkxvx0CXeDZuEz3kKgVsQpyNRXP0AWKS8FqxbVe+NLDZl9jzDtQBvCt +BW3UjgIxnUMG5tGrtTHvzLJNfkX4DvTkWUOiLu8VxcAT6vG5v87xyrq86taMLrCm +o42wHpJNETPlCtJquGEADPHs4D+EOAMWfaCvy5rEYt6JD/oy6/VehPInE3Q634NK +98OD25xykOtq1sIvoZibIgq8T5GlmBrxAb+axfhX8tp7NEJ4wvOUorEnww3cZJb6 +7Q6ijvzEWT2RD58prR7l4yvsb+HmSEWoJcd5JZd0CUiVIM44L3E4qllXJ2vwT9Fv +dXwfwx29zQ9VVLlNibPzJ4a0ZEIhi8ZMGtoLR7gEhsM+PraZz+2TL5APJWI2TRZk +A2m7v2DMv4ZbUOOy1OivOIwJqA6k9lsm2oMOPrTIZ0pZIXu1bmF0/Z8js4LNx5HU +4SsIHYpj1efH27YgjfBY9ROX3iITQsKyUpCpF9z845tzbdw66tRTazLvumNVrdlz +fKceFDbmgi6uz/lILSppy/kzKkoaY6+NTogW6Kg/2icPH8rg6LpxHpQlAHqABB8t +25w39DQoD5sPGt/ChEX5Pb0bLUWlnl+6vKa4vfctiZ2DevGePLrK1lW+L4K05yCF +CpqPWKvm75aVs9HZGFbIsJTF2B4IQnYmm9IhqFuSh1sZBf2dAgMBAAGjgYYwgYMw +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFHSvVlBTGCMOip+SGZ7QO+0isE8tMCwGA1UdEQQlMCOC +CW9yYXMubGFuZIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsF +AAOCAgEANrLbIqIe+7hd51BZDJn1tSE/Ld/kgwJ8mBZdB6XF2WLoCgVOx9VGmJH3 +Z09b0KqJpp3s2HgYJNc5AHNAyH/u1E4HUAyKajuI420Z/GdV3CK2uwcp5mkkF4qK +ew6PEYUQRjZf95k+6T8VR8P9O0kiigr4Srgqal2EolSf8e27EO+JERueVn3NgRms +FrJv2+LILKFfBt5o9S1D5xNa9qu6NfW3x6F0VwaBxgjnUpqePUQaFzSSZ+VXmKmh +rx7I8PaNrac5DslDtmpVj/z9RwuExPZAB+/utocUWmSU0epTyqXiKQy8sgSVhsVe +yyClPguXtuD3IV/i5uAcSVf1xrCSSifkYnmhfMA5lUgjOmDuY/Am4f2DJcjBETb2 +hsac+8C6IppGrZOuOinTp5ZIocFKjNxcjGnmqxk0hWW6GuF4truKSEicIiBvoqZp +rearfvDbM6g4CobowU1S6vNkUc2ziCE23AmoY65V3Vnmj72p5Mi7P932jzf2qEA1 +vCGB9hrxO3Yr5wkOXUGQI/nU+KQTCdf/4j4kUh+N1pByGMPG05GrOpmy1Tems9sp +BddDm85WsThpxTf+Rp5xp5/FQh72eigkXa/ezeldEGI/rhKRJJtujU6Kq6SoKX29 +YRSTpM+MeFPJDVffVUFXQrxnGIXRzxhx49wMSoKFYR/225exX1c= +-----END CERTIFICATE-----`) + +// localhostServerKey is the private key for localhostServerCert. +var localhostServerKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- +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 +MIoYYNqQ9tdHlLwyViIAgaZvunID5Q== +-----END RSA TESTING KEY-----`)) + +// localhostClientCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// adapted from src/crypto/tls: +// go run generate_cert.go --rsa-bits 4096 --host 127.0.0.1,::1,oras.land --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostClientCert = []byte(`-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgIRAMow8iHaN1G+R2GjMyOC5LQwDQYJKoZIhvcNAQELBQAw +DzENMAsGA1UEChMET1JBUzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2MDAw +MFowDzENMAsGA1UEChMET1JBUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAM01pjqP4xZmX8zAT5p7k90FpbZ1W5S8qLIkajvzzoGeksSH2FgG+rXK+yJ6 +TZo74a5ZKOUHtdjurRTvMgCjCvFIWv0wqnkeVFmXDLXQZiEamatPInGF2vJnv9BW +O4sLdopyXxAd4BQiC7ot8JO2PRiLubPRHDp2xExsDbqiAPU49qvwVFEfdnU+nHdt +jmmcZKzeD4iiu9DeZZJhv9h+OEZNXyHNDFV9/CRNrwO9Xfx8OvTZNklSYOyF0YER +nr1mmdQBZ4SCYYGBBdWvjkII7hZNkT9/qOBB2p/JXquF+6GYGruVAfAYRfADXCg1 ++T+M8X1XZuja5wIxvwbH8JV5RXN/vk9mt36EXGSuesAOlVYKnv3Ux1bxPSuWT3RU +47GM/8vmelopHTIUtcLPnNFk3xlWyjNx7Sv67CxWniJFuOXrqrI0DLj/EQzOxoMa +uD0WO0BXHHNSFF7dhrd6FTNu3EppYFWQt9wBsZ8jgDRsHh2GeuyEo1YlSSB+rvs4 +20w9cCSYGduJ4BKqcGRDY6c9QCRgZ5VWs+feS3awBK6Gx4mWh4bjM1ICGbiPeiw9 +2DeHjjAqunlMN0z7Pa28lE0GE3rZpcwxtS3Blnm6Q/lDbCBmOWrGH4ngQ02HeaY4 +t7xUayaiz3lkZjcTrdZa9OxHLZwmGmqY7JB3X/TReePr9jT5AgMBAAGjgYYwgYMw +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMCMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFDpedkP2AJKzCgJkZf9PpIOIXRHbMCwGA1UdEQQlMCOC +CW9yYXMubGFuZIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsF +AAOCAgEAGtRCFG27HZqu2kz0IW6UuYUKv7gqtN4/47At3CHFAd3UAAErKrammwFH +fWB4zy4MU9fHMgJhc8ctZg0QR8f1fnpN8MlnmV3fqEbUVwbZov2x46wgwLD7PRZM +NPh+Z2reFRE2VNi+rZsqcTAPy48FQ0WdCpzQheeonincLuY6GYV1DjhHn3HNFqkn +lslS5FRRkkLxXLgr0p0fNAEUMqce16VjVcM2BbEuCRvlepZZF7uVvEOQvtEMJSdF +APmUUsS7x6jvIxmZpxzanBmu2u4kQwTMW4UDpkNVwdCPGEM/SAdQUkqOYIQU807n +93QrXXY3XIlDp7U6xA2AfZQ9fiV7cDul06IzsXcisTeK8uSfhHRROqbt9MotiIve +xEVTyh4tfue7kFowq7wd3H2UTSLgByfJBkEAN+BTDUpEwT7gZOVcxbzpkL+vyPfr +CVdIbE9ZlBzCc5i0RWaMrEJfFy93a7ZptlXytfQM/MFOlKaIDXFjoylmkW9IhrFd +Ger/gr4AFzD7AQRF9+c0Cu4je3A+ob9R0CvvwEthjQCrOWjVgbUxR+qMVNCtCRwk +Bt4BEd7qH0bM8EAY7BwPEP7vfvp7iblp0FNNLYQCbxd0eK69Tb2olzQRKKyNpqls +/cqUr8lpYbGAdorLJSeCj8EZ7+o9ZJXbxilkcMl3R8wV8mn5Olw= +-----END CERTIFICATE-----`) + +// localhostClientKey is the private key for localhostClientCert. +var localhostClientKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- +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 +tFAnaOX7ziz6wrV8TmuU9ZdGX+Aw+g== +-----END RSA TESTING KEY-----`)) + +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +func loadTestingTLSConfig() *tls.Config { + + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(localhostClientCert) + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{loadTestingCert(localhostServerCert, localhostServerKey)}, + ClientAuth: tls.VerifyClientCertIfGiven, + ClientCAs: clientCertPool, + } + + return tlsConfig +} + +func loadTestingCert(certificate, key []byte) tls.Certificate { + cert, err := tls.X509KeyPair(certificate, key) + if err != nil { + panic(fmt.Sprintf("Unable to load testing certificate: %v", err)) + } + + return cert + +} + func TestMain(m *testing.M) { // Test server - ts = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := r.URL.Path m := r.Method switch { @@ -57,6 +263,8 @@ func TestMain(m *testing.M) { } } })) + ts.TLS = loadTestingTLSConfig() + ts.StartTLS() defer ts.Close() m.Run() } @@ -116,7 +324,7 @@ func TestRemote_authClient_skipTlsVerify(t *testing.T) { func TestRemote_authClient_CARoots(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") - if err := os.WriteFile(caPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ts.Certificate().Raw}), 0644); err != nil { + if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -174,7 +382,7 @@ func plainHTTPNotSpecified() (plainHTTP bool, fromFlag bool) { func TestRemote_NewRegistry(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") - if err := os.WriteFile(caPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ts.Certificate().Raw}), 0644); err != nil { + if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -203,15 +411,63 @@ func TestRemote_NewRegistry(t *testing.T) { func TestRemote_NewRepository(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") - if err := os.WriteFile(caPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ts.Certificate().Raw}), 0644); err != nil { + if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { + t.Fatalf("unexpected error: %v", err) + } + opts := struct { + Remote + Common + }{ + Remote{ + CACertFilePath: caPath, + plainHTTP: plainHTTPNotSpecified, + }, + Common{}, + } + + uri, err := url.ParseRequestURI(ts.URL) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + repo, err := opts.NewRepository(uri.Host+"/"+testRepo, opts.Common, logrus.New()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err = repo.Tags(context.Background(), "", func(got []string) error { + want := []string{"tag"} + if len(got) != len(testTagList.Tags) || !reflect.DeepEqual(got, want) { + return fmt.Errorf("expect: %v, got: %v", testTagList.Tags, got) + } + return nil + }); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRemote_NewRepositoryMTLS(t *testing.T) { + caPath := filepath.Join(t.TempDir(), "oras-test.pem") + if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + clientCertPath := filepath.Join(t.TempDir(), "oras-test-client.pem") + if err := os.WriteFile(clientCertPath, localhostClientCert, 0644); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + clientKeyPath := filepath.Join(t.TempDir(), "oras-test-client.key") + if err := os.WriteFile(clientKeyPath, localhostClientKey, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } + opts := struct { Remote Common }{ Remote{ CACertFilePath: caPath, + CertFilePath: clientCertPath, + KeyFilePath: clientKeyPath, plainHTTP: plainHTTPNotSpecified, }, Common{}, @@ -238,11 +494,11 @@ func TestRemote_NewRepository(t *testing.T) { func TestRemote_NewRepository_Retry(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") - if err := os.WriteFile(caPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ts.Certificate().Raw}), 0644); err != nil { + if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } retries, count := 3, 0 - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count++ if count < retries { http.Error(w, "error", http.StatusTooManyRequests) @@ -253,6 +509,8 @@ func TestRemote_NewRepository_Retry(t *testing.T) { http.Error(w, "error encoding", http.StatusBadRequest) } })) + ts.TLS = loadTestingTLSConfig() + ts.StartTLS() defer ts.Close() opts := struct { Remote