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

feat: add mTLS options #1319

Merged
merged 1 commit into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 19 additions & 1 deletion cmd/oras/internal/option/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
)

const (
caFileFlag = "ca-file"
certFileFlag = "cert-file"
keyFileFlag = "key-file"
usernameFlag = "username"
passwordFlag = "password"
passwordFromStdinFlag = "password-stdin"
Expand All @@ -58,6 +61,8 @@
type Remote struct {
DistributionSpec
CACertFilePath string
CertFilePath string
KeyFilePath string
Insecure bool
Configs []string
Username string
Expand Down Expand Up @@ -120,7 +125,9 @@
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 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")
Expand All @@ -142,6 +149,7 @@
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)
Expand All @@ -155,6 +163,9 @@
if err := opts.parseCustomHeaders(); err != nil {
return err
}

cmd.MarkFlagsRequiredTogether(certFileAndKeyFileFlags...)

return opts.readSecret(cmd)
}

Expand Down Expand Up @@ -228,6 +239,13 @@
return nil, err
}
}
if opts.CertFilePath != "" && opts.KeyFilePath != "" {
qweeah marked this conversation as resolved.
Show resolved Hide resolved
cert, err := tls.LoadX509KeyPair(opts.CertFilePath, opts.KeyFilePath)
if err != nil {
return nil, err

Check warning on line 245 in cmd/oras/internal/option/remote.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/option/remote.go#L245

Added line #L245 was not covered by tests
}
config.Certificates = []tls.Certificate{cert}
}
return config, nil
}

Expand Down
123 changes: 116 additions & 7 deletions cmd/oras/internal/option/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ limitations under the License.
package option

import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
_ "embed"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
Expand All @@ -43,9 +46,63 @@ 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 golang 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
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
//
//go:embed testdata/localhostServer.crt
var localhostServerCert []byte

// localhostServerKey is the private key for localhostServerCert.
//
//go:embed testdata/localhostServer.key
var localhostServerKey []byte

// 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 golang crypto/tls (added Client Auth usage):
// 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
//
//go:embed testdata/localhostClient.crt
var localhostClientCert []byte

// localhostClientKey is the private key for localhostClientCert.
//
//go:embed testdata/localhostClient.key
var localhostClientKey []byte

func testingKey(s []byte) []byte {
return bytes.ReplaceAll(s, []byte("TESTING KEY"), []byte("PRIVATE KEY"))
}

func loadTestingTLSConfig() *tls.Config {

clientCertPool := x509.NewCertPool()
clientCertPool.AppendCertsFromPEM(localhostClientCert)

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{loadTestingCert(localhostServerCert, testingKey(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

shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
}

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 {
Expand All @@ -57,6 +114,8 @@ func TestMain(m *testing.M) {
}
}
}))
ts.TLS = loadTestingTLSConfig()
ts.StartTLS()
defer ts.Close()
m.Run()
}
Expand Down Expand Up @@ -116,7 +175,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)
}

Expand Down Expand Up @@ -174,7 +233,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)
}

Expand Down Expand Up @@ -203,15 +262,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, testingKey(localhostClientKey), 0644); err != nil {
t.Fatalf("unexpected error: %v", err)
}

opts := struct {
Remote
Common
}{
Remote{
CACertFilePath: caPath,
CertFilePath: clientCertPath,
KeyFilePath: clientKeyPath,
plainHTTP: plainHTTPNotSpecified,
},
Common{},
Expand All @@ -238,11 +345,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)
Expand All @@ -253,6 +360,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
Expand Down
30 changes: 30 additions & 0 deletions cmd/oras/internal/option/testdata/localhostClient.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----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-----
52 changes: 52 additions & 0 deletions cmd/oras/internal/option/testdata/localhostClient.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN RSA TESTING KEY-----
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
tFAnaOX7ziz6wrV8TmuU9ZdGX+Aw+g==
-----END RSA TESTING KEY-----
30 changes: 30 additions & 0 deletions cmd/oras/internal/option/testdata/localhostServer.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----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-----
Loading
Loading