From 92102d0f34f675b9dcc9326948264887e852af64 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 May 2022 10:37:51 +0800 Subject: [PATCH 1/8] Support certificate authority configuration Signed-off-by: Billy Zha --- cmd/oras/login.go | 21 ++++++++++++------- internal/http/certificate.go | 24 ++++++++++++++++++++++ internal/http/client.go | 3 +++ internal/http/client_test.go | 39 +++++++++++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 internal/http/certificate.go diff --git a/cmd/oras/login.go b/cmd/oras/login.go index 37b1b8841..b93038b6a 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -38,13 +38,14 @@ type loginOptions struct { hostname string fromStdin bool - debug bool - configs []string - username string - password string - insecure bool - plainHttp bool - verbose bool + debug bool + configs []string + caFilePaths []string + username string + password string + insecure bool + plainHttp bool + verbose bool } func loginCmd() *cobra.Command { @@ -85,6 +86,7 @@ Example - Login with insecure registry from command line: cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password or identity token") cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs") + cmd.Flags().StringArrayVarP(&opts.caFilePaths, "insecure-ca-files", "", nil, "allow connections to SSL registry without certs") cmd.Flags().BoolVarP(&opts.plainHttp, "plain-http", "", false, "allow insecure connections to registry without SSL") cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output") return cmd @@ -147,10 +149,15 @@ func runLogin(opts loginOptions) (err error) { } remote.PlainHTTP = opts.plainHttp cred := credential.Credential(opts.username, opts.password) + rootCAs, err := http.LoadRootCAs(opts.caFilePaths) + if err != nil { + return err + } remote.Client = http.NewClient(http.ClientOptions{ Credential: cred, SkipTLSVerify: opts.insecure, Debug: opts.debug, + RootCAs: rootCAs, }) if err = remote.Ping(ctx); err != nil { return err diff --git a/internal/http/certificate.go b/internal/http/certificate.go new file mode 100644 index 000000000..ac38e2af0 --- /dev/null +++ b/internal/http/certificate.go @@ -0,0 +1,24 @@ +package http + +import ( + "crypto/x509" + "errors" + "os" +) + +func LoadRootCAs(paths []string) (pool *x509.CertPool, err error) { + pool, err = x509.SystemCertPool() + if err != nil { + return nil, err + } + for _, path := range paths { + pemBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if ok := pool.AppendCertsFromPEM(pemBytes); !ok { + return nil, errors.New("Failed to add certificate authority in file: " + path) + } + } + return pool, nil +} diff --git a/internal/http/client.go b/internal/http/client.go index b3ae7cebf..9eb392a5a 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -3,6 +3,7 @@ package http import ( "context" "crypto/tls" + "crypto/x509" "net/http" "oras.land/oras-go/v2/registry/remote" @@ -18,6 +19,7 @@ type ClientOptions struct { CredentialStore *credential.Store SkipTLSVerify bool Debug bool + RootCAs *x509.CertPool } func NewClient(opts ClientOptions) remote.Client { @@ -26,6 +28,7 @@ func NewClient(opts ClientOptions) remote.Client { Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: opts.SkipTLSVerify, + RootCAs: opts.RootCAs, }, }, }, diff --git a/internal/http/client_test.go b/internal/http/client_test.go index 66989c51c..c4507ee16 100644 --- a/internal/http/client_test.go +++ b/internal/http/client_test.go @@ -15,9 +15,13 @@ limitations under the License. package http_test import ( + "context" + "crypto/x509" + "encoding/pem" "testing" nhttp "net/http" + "net/http/httptest" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras/internal/http" @@ -31,6 +35,7 @@ func Test_NewClient_credential(t *testing.T) { Credential: wanted, } client := http.NewClient(opts) + got, err := client.(*auth.Client).Credential(nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -41,16 +46,48 @@ func Test_NewClient_credential(t *testing.T) { } } -func Test_NewClient_tlsConfig(t *testing.T) { +func Test_NewClient_skipTlsVerify(t *testing.T) { opts := http.ClientOptions{ SkipTLSVerify: true, } wanted := opts.SkipTLSVerify client := http.NewClient(opts) + config := client.(*auth.Client).Client.Transport.(*nhttp.Transport).TLSClientConfig got := config.InsecureSkipVerify if got != wanted { t.Fatalf("expect: %v, got: %v", wanted, got) } } + +func Test_NewClient_CARoots(t *testing.T) { + // Test server + ts := httptest.NewTLSServer(nhttp.HandlerFunc(func(w nhttp.ResponseWriter, r *nhttp.Request) { + p := r.URL.Path + m := r.Method + switch { + case p == "/v2/" && m == "GET": + w.WriteHeader(nhttp.StatusOK) + } + })) + defer ts.Close() + + // Test CA pool + pool := x509.NewCertPool() + c := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ts.Certificate().Raw}) + pool.AppendCertsFromPEM(c) + opts := http.ClientOptions{ + RootCAs: pool, + } + + client := http.NewClient(opts) + req, err := nhttp.NewRequestWithContext(context.Background(), nhttp.MethodGet, ts.URL, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = client.Do(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} From e0b58371ecda81bf50f775b719c003b7a1c575a2 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 May 2022 11:39:47 +0800 Subject: [PATCH 2/8] Change option type and rename Signed-off-by: Billy Zha --- cmd/oras/login.go | 20 ++++++++++---------- internal/http/certificate.go | 16 +++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/cmd/oras/login.go b/cmd/oras/login.go index b93038b6a..aaee0c752 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -38,14 +38,14 @@ type loginOptions struct { hostname string fromStdin bool - debug bool - configs []string - caFilePaths []string - username string - password string - insecure bool - plainHttp bool - verbose bool + debug bool + configs []string + caFilePath string + username string + password string + insecure bool + plainHttp bool + verbose bool } func loginCmd() *cobra.Command { @@ -86,7 +86,7 @@ Example - Login with insecure registry from command line: cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password or identity token") cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs") - cmd.Flags().StringArrayVarP(&opts.caFilePaths, "insecure-ca-files", "", nil, "allow connections to SSL registry without certs") + cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "allow connections to SSL registry without certs") cmd.Flags().BoolVarP(&opts.plainHttp, "plain-http", "", false, "allow insecure connections to registry without SSL") cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output") return cmd @@ -149,7 +149,7 @@ func runLogin(opts loginOptions) (err error) { } remote.PlainHTTP = opts.plainHttp cred := credential.Credential(opts.username, opts.password) - rootCAs, err := http.LoadRootCAs(opts.caFilePaths) + rootCAs, err := http.LoadRootCAs(opts.caFilePath) if err != nil { return err } diff --git a/internal/http/certificate.go b/internal/http/certificate.go index ac38e2af0..ae0ae30ff 100644 --- a/internal/http/certificate.go +++ b/internal/http/certificate.go @@ -6,19 +6,17 @@ import ( "os" ) -func LoadRootCAs(paths []string) (pool *x509.CertPool, err error) { +func LoadRootCAs(path string) (pool *x509.CertPool, err error) { pool, err = x509.SystemCertPool() if err != nil { return nil, err } - for _, path := range paths { - pemBytes, err := os.ReadFile(path) - if err != nil { - return nil, err - } - if ok := pool.AppendCertsFromPEM(pemBytes); !ok { - return nil, errors.New("Failed to add certificate authority in file: " + path) - } + pemBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if ok := pool.AppendCertsFromPEM(pemBytes); !ok { + return nil, errors.New("Failed to add certificate authority in file: " + path) } return pool, nil } From bb1f8721ea460853397b085b143ac96613f38c34 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 May 2022 11:49:52 +0800 Subject: [PATCH 3/8] Change flag description Signed-off-by: Billy Zha --- cmd/oras/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/login.go b/cmd/oras/login.go index aaee0c752..6c73b9c33 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -86,7 +86,7 @@ Example - Login with insecure registry from command line: cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password or identity token") cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs") - cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "allow connections to SSL registry without certs") + cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "user specified certificate authority file for the registry client") cmd.Flags().BoolVarP(&opts.plainHttp, "plain-http", "", false, "allow insecure connections to registry without SSL") cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output") return cmd From 231a91fe27818050ce9a6061a32f87be1df819d6 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 May 2022 11:50:32 +0800 Subject: [PATCH 4/8] Update flag description Signed-off-by: Billy Zha --- cmd/oras/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/login.go b/cmd/oras/login.go index 6c73b9c33..dadc12713 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -86,7 +86,7 @@ Example - Login with insecure registry from command line: cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password or identity token") cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs") - cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "user specified certificate authority file for the registry client") + cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "user specified certificate authority file for the registry target") cmd.Flags().BoolVarP(&opts.plainHttp, "plain-http", "", false, "allow insecure connections to registry without SSL") cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output") return cmd From b07fd1d3e6242bc7a93fa5d88dca4d071ce039ad Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 May 2022 11:52:53 +0800 Subject: [PATCH 5/8] Code clean Signed-off-by: Billy Zha --- internal/http/client_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/http/client_test.go b/internal/http/client_test.go index c4507ee16..797ef7502 100644 --- a/internal/http/client_test.go +++ b/internal/http/client_test.go @@ -35,7 +35,6 @@ func Test_NewClient_credential(t *testing.T) { Credential: wanted, } client := http.NewClient(opts) - got, err := client.(*auth.Client).Credential(nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -53,7 +52,6 @@ func Test_NewClient_skipTlsVerify(t *testing.T) { wanted := opts.SkipTLSVerify client := http.NewClient(opts) - config := client.(*auth.Client).Client.Transport.(*nhttp.Transport).TLSClientConfig got := config.InsecureSkipVerify if got != wanted { From db63fe95581c2c93d8481fbe3e83d6557316f8e7 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 May 2022 11:56:08 +0800 Subject: [PATCH 6/8] Code clean Signed-off-by: Billy Zha --- internal/http/certificate.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/http/certificate.go b/internal/http/certificate.go index ae0ae30ff..37dee47a8 100644 --- a/internal/http/certificate.go +++ b/internal/http/certificate.go @@ -6,8 +6,8 @@ import ( "os" ) -func LoadRootCAs(path string) (pool *x509.CertPool, err error) { - pool, err = x509.SystemCertPool() +func LoadRootCAs(path string) (*x509.CertPool, error) { + pool, err := x509.SystemCertPool() if err != nil { return nil, err } From 677516376c171be5d03ef5829ae1ba7249d5cbdc Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 16 May 2022 18:22:45 +0800 Subject: [PATCH 7/8] Resolve comments Signed-off-by: Billy Zha --- cmd/oras/login.go | 4 ++-- internal/http/certificate.go | 9 +++------ internal/http/client_test.go | 4 +--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cmd/oras/login.go b/cmd/oras/login.go index dadc12713..e7274ecb3 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -86,7 +86,7 @@ Example - Login with insecure registry from command line: cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password or identity token") cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs") - cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "user specified certificate authority file for the registry target") + cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "server certificate authority file for the remote registry") cmd.Flags().BoolVarP(&opts.plainHttp, "plain-http", "", false, "allow insecure connections to registry without SSL") cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output") return cmd @@ -149,7 +149,7 @@ func runLogin(opts loginOptions) (err error) { } remote.PlainHTTP = opts.plainHttp cred := credential.Credential(opts.username, opts.password) - rootCAs, err := http.LoadRootCAs(opts.caFilePath) + rootCAs, err := http.LoadCertPool(opts.caFilePath) if err != nil { return err } diff --git a/internal/http/certificate.go b/internal/http/certificate.go index 37dee47a8..2b3089522 100644 --- a/internal/http/certificate.go +++ b/internal/http/certificate.go @@ -6,17 +6,14 @@ import ( "os" ) -func LoadRootCAs(path string) (*x509.CertPool, error) { - pool, err := x509.SystemCertPool() - if err != nil { - return nil, err - } +func LoadCertPool(path string) (*x509.CertPool, error) { + pool := x509.NewCertPool() pemBytes, err := os.ReadFile(path) if err != nil { return nil, err } if ok := pool.AppendCertsFromPEM(pemBytes); !ok { - return nil, errors.New("Failed to add certificate authority in file: " + path) + return nil, errors.New("Failed to load certificate in file: " + path) } return pool, nil } diff --git a/internal/http/client_test.go b/internal/http/client_test.go index 797ef7502..7d7e08042 100644 --- a/internal/http/client_test.go +++ b/internal/http/client_test.go @@ -17,7 +17,6 @@ package http_test import ( "context" "crypto/x509" - "encoding/pem" "testing" nhttp "net/http" @@ -73,8 +72,7 @@ func Test_NewClient_CARoots(t *testing.T) { // Test CA pool pool := x509.NewCertPool() - c := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ts.Certificate().Raw}) - pool.AppendCertsFromPEM(c) + pool.AddCert(ts.Certificate()) opts := http.ClientOptions{ RootCAs: pool, } From 033f24101c588a4da74c501560ab37e7390aacb8 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 16 May 2022 18:26:59 +0800 Subject: [PATCH 8/8] Code clean Signed-off-by: Billy Zha --- cmd/oras/login.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/oras/login.go b/cmd/oras/login.go index e7274ecb3..7a65d3758 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -44,7 +44,7 @@ type loginOptions struct { username string password string insecure bool - plainHttp bool + plainHTTP bool verbose bool } @@ -87,7 +87,7 @@ Example - Login with insecure registry from command line: cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs") cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "server certificate authority file for the remote registry") - cmd.Flags().BoolVarP(&opts.plainHttp, "plain-http", "", false, "allow insecure connections to registry without SSL") + cmd.Flags().BoolVarP(&opts.plainHTTP, "plain-http", "", false, "allow insecure connections to registry without SSL") cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output") return cmd } @@ -147,7 +147,7 @@ func runLogin(opts loginOptions) (err error) { if err != nil { return err } - remote.PlainHTTP = opts.plainHttp + remote.PlainHTTP = opts.plainHTTP cred := credential.Credential(opts.username, opts.password) rootCAs, err := http.LoadCertPool(opts.caFilePath) if err != nil {