From 1ed776ff5213a7c3b8a08621168aecfd2e2cd081 Mon Sep 17 00:00:00 2001 From: Tiago Scolari Date: Tue, 3 Oct 2017 13:40:56 +0100 Subject: [PATCH] Support for remote OCI layers if they have a URL * Support for custom certs and keys when downloading OCI layers Signed-off-by: Will Martin --- docker/docker_client.go | 94 +------------ oci/layout/fixtures/accepted_certs/cacert.crt | 13 ++ oci/layout/fixtures/accepted_certs/cert.cert | 13 ++ oci/layout/fixtures/accepted_certs/cert.key | 7 + oci/layout/fixtures/manifest/index.json | 1 + oci/layout/fixtures/rejected_certs/cert.cert | 13 ++ oci/layout/fixtures/rejected_certs/cert.key | 7 + oci/layout/oci_dest.go | 2 +- oci/layout/oci_src.go | 55 +++++++- oci/layout/oci_src_test.go | 132 ++++++++++++++++++ oci/layout/oci_transport.go | 4 +- pkg/tlsclientconfig/tlsclientconfig.go | 102 ++++++++++++++ types/types.go | 8 ++ 13 files changed, 357 insertions(+), 94 deletions(-) create mode 100644 oci/layout/fixtures/accepted_certs/cacert.crt create mode 100644 oci/layout/fixtures/accepted_certs/cert.cert create mode 100644 oci/layout/fixtures/accepted_certs/cert.key create mode 100644 oci/layout/fixtures/manifest/index.json create mode 100644 oci/layout/fixtures/rejected_certs/cert.cert create mode 100644 oci/layout/fixtures/rejected_certs/cert.key create mode 100644 oci/layout/oci_src_test.go create mode 100644 pkg/tlsclientconfig/tlsclientconfig.go diff --git a/docker/docker_client.go b/docker/docker_client.go index 4c3d8b9fe9..511490407f 100644 --- a/docker/docker_client.go +++ b/docker/docker_client.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "io/ioutil" - "net" "net/http" "os" "path/filepath" @@ -16,11 +15,11 @@ import ( "time" "github.com/containers/image/docker/reference" + "github.com/containers/image/pkg/tlsclientconfig" "github.com/containers/image/types" "github.com/containers/storage/pkg/homedir" "github.com/docker/distribution/registry/client" helperclient "github.com/docker/docker-credential-helpers/client" - "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -113,27 +112,7 @@ func serverDefault() *tls.Config { } } -func newTransport() *http.Transport { - direct := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - } - tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: direct.Dial, - TLSHandshakeTimeout: 10 * time.Second, - // TODO(dmcgowan): Call close idle connections when complete and use keep alive - DisableKeepAlives: true, - } - proxyDialer, err := sockets.DialerFromEnvironment(direct) - if err == nil { - tr.Dial = proxyDialer.Dial - } - return tr -} - -// dockerCertDir returns a path to a directory to be consumed by setupCertificates() depending on ctx and hostPort. +// dockerCertDir returns a path to a directory to be consumed by tlsclientconfig.SetupCertificates() depending on ctx and hostPort. func dockerCertDir(ctx *types.SystemContext, hostPort string) string { if ctx != nil && ctx.DockerCertPath != "" { return ctx.DockerCertPath @@ -149,69 +128,6 @@ func dockerCertDir(ctx *types.SystemContext, hostPort string) string { return filepath.Join(hostCertDir, hostPort) } -func setupCertificates(dir string, tlsc *tls.Config) error { - logrus.Debugf("Looking for TLS certificates and private keys in %s", dir) - fs, err := ioutil.ReadDir(dir) - if err != nil { - if os.IsNotExist(err) { - return nil - } - if os.IsPermission(err) { - logrus.Debugf("Skipping scan of %s due to permission error: %v", dir, err) - return nil - } - return err - } - - for _, f := range fs { - fullPath := filepath.Join(dir, f.Name()) - if strings.HasSuffix(f.Name(), ".crt") { - systemPool, err := tlsconfig.SystemCertPool() - if err != nil { - return errors.Wrap(err, "unable to get system cert pool") - } - tlsc.RootCAs = systemPool - logrus.Debugf(" crt: %s", fullPath) - data, err := ioutil.ReadFile(fullPath) - if err != nil { - return err - } - tlsc.RootCAs.AppendCertsFromPEM(data) - } - if strings.HasSuffix(f.Name(), ".cert") { - certName := f.Name() - keyName := certName[:len(certName)-5] + ".key" - logrus.Debugf(" cert: %s", fullPath) - if !hasFile(fs, keyName) { - return errors.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName) - } - cert, err := tls.LoadX509KeyPair(filepath.Join(dir, certName), filepath.Join(dir, keyName)) - if err != nil { - return err - } - tlsc.Certificates = append(tlsc.Certificates, cert) - } - if strings.HasSuffix(f.Name(), ".key") { - keyName := f.Name() - certName := keyName[:len(keyName)-4] + ".cert" - logrus.Debugf(" key: %s", fullPath) - if !hasFile(fs, certName) { - return errors.Errorf("missing client certificate %s for key %s", certName, keyName) - } - } - } - return nil -} - -func hasFile(files []os.FileInfo, name string) bool { - for _, f := range files { - if f.Name() == name { - return true - } - } - return false -} - // newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) // “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection) func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) { @@ -223,7 +139,7 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, if err != nil { return nil, err } - tr := newTransport() + tr := tlsclientconfig.NewTransport() tr.TLSClientConfig = serverDefault() // It is undefined whether the host[:port] string for dockerHostname should be dockerHostname or dockerRegistry, // because docker/docker does not read the certs.d subdirectory at all in that case. We use the user-visible @@ -231,7 +147,7 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, // generally the UI hides the existence of the different dockerRegistry. But note that this behavior is // undocumented and may change if docker/docker changes. certDir := dockerCertDir(ctx, reference.Domain(ref.ref)) - if err := setupCertificates(certDir, tr.TLSClientConfig); err != nil { + if err := tlsclientconfig.SetupCertificates(certDir, tr.TLSClientConfig); err != nil { return nil, err } if ctx != nil && ctx.DockerInsecureSkipTLSVerify { @@ -364,7 +280,7 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope if c.username != "" && c.password != "" { authReq.SetBasicAuth(c.username, c.password) } - tr := newTransport() + tr := tlsclientconfig.NewTransport() // TODO(runcom): insecure for now to contact the external token service tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} client := &http.Client{Transport: tr} diff --git a/oci/layout/fixtures/accepted_certs/cacert.crt b/oci/layout/fixtures/accepted_certs/cacert.crt new file mode 100644 index 0000000000..cfcc58ad89 --- /dev/null +++ b/oci/layout/fixtures/accepted_certs/cacert.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICCjCCAWygAwIBAgIQFg60+EkXWgVv8xqI3yrxQjAKBggqhkjOPQQDBDASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE3MDkyMjEwMzkzOFoXDTE4MDkyMjEwMzkzOFow +EjEQMA4GA1UEChMHQWNtZSBDbzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAI3p +xckijV44L3ffAlLOqB4oA/HpP7S5gTpWrIUU+2SxFJU/bcTKDLPk1cEC87vW+UCY +IXAyYGlyMAGSm0GxAFHnAIIrQzx9m3yiHbUyIPvRMW4BoDKsLaf5+GIZMm9nOq2q +njvHr9ag2J3IzxEqQ8KZ95ivmHYrh3VsnfisI7c3opiro2EwXzAOBgNVHQ8BAf8E +BAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0RBBYwFIIJbG9jYWxob3N0gQdhQGEuY29tMAoGCCqGSM49BAME +A4GLADCBhwJCAbk0YzSo4Pf673WlVJ5kitJ1ti0Y6NT47up5683/Y6V2/WM668Zb +x0qVoa1KUKjqpmdcNS22efqB0P2Ns+2kdh4XAkEm1toJclyhucvTYIsD6MlAxp6v +Ji5mpzROSkxZVsuH2BoYuJMjkjLeRImekPKHvYzmx4yoMyO4h4NEmTtkp3RdjQ== +-----END CERTIFICATE----- diff --git a/oci/layout/fixtures/accepted_certs/cert.cert b/oci/layout/fixtures/accepted_certs/cert.cert new file mode 100644 index 0000000000..cfcc58ad89 --- /dev/null +++ b/oci/layout/fixtures/accepted_certs/cert.cert @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICCjCCAWygAwIBAgIQFg60+EkXWgVv8xqI3yrxQjAKBggqhkjOPQQDBDASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE3MDkyMjEwMzkzOFoXDTE4MDkyMjEwMzkzOFow +EjEQMA4GA1UEChMHQWNtZSBDbzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAI3p +xckijV44L3ffAlLOqB4oA/HpP7S5gTpWrIUU+2SxFJU/bcTKDLPk1cEC87vW+UCY +IXAyYGlyMAGSm0GxAFHnAIIrQzx9m3yiHbUyIPvRMW4BoDKsLaf5+GIZMm9nOq2q +njvHr9ag2J3IzxEqQ8KZ95ivmHYrh3VsnfisI7c3opiro2EwXzAOBgNVHQ8BAf8E +BAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0RBBYwFIIJbG9jYWxob3N0gQdhQGEuY29tMAoGCCqGSM49BAME +A4GLADCBhwJCAbk0YzSo4Pf673WlVJ5kitJ1ti0Y6NT47up5683/Y6V2/WM668Zb +x0qVoa1KUKjqpmdcNS22efqB0P2Ns+2kdh4XAkEm1toJclyhucvTYIsD6MlAxp6v +Ji5mpzROSkxZVsuH2BoYuJMjkjLeRImekPKHvYzmx4yoMyO4h4NEmTtkp3RdjQ== +-----END CERTIFICATE----- diff --git a/oci/layout/fixtures/accepted_certs/cert.key b/oci/layout/fixtures/accepted_certs/cert.key new file mode 100644 index 0000000000..b221f74b77 --- /dev/null +++ b/oci/layout/fixtures/accepted_certs/cert.key @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIAMDtdVU5PeUWCo1Ndvr+1X+Hry4I7+NdTqxLlU0ZBudm2ov0iJdZj +O2PdSW6pRHJl9gYL+D/QjcEIwQBK4vsHS3SgBwYFK4EEACOhgYkDgYYABACN6cXJ +Io1eOC933wJSzqgeKAPx6T+0uYE6VqyFFPtksRSVP23Eygyz5NXBAvO71vlAmCFw +MmBpcjABkptBsQBR5wCCK0M8fZt8oh21MiD70TFuAaAyrC2n+fhiGTJvZzqtqp47 +x6/WoNidyM8RKkPCmfeYr5h2K4d1bJ34rCO3N6KYqw== +-----END EC PRIVATE KEY----- diff --git a/oci/layout/fixtures/manifest/index.json b/oci/layout/fixtures/manifest/index.json new file mode 100644 index 0000000000..fd6930cf1c --- /dev/null +++ b/oci/layout/fixtures/manifest/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:84afb6189c4d69f2d040c5f1dc4e0a16fed9b539ce9cfb4ac2526ae4e0576cc0","size":496,"annotations":{"org.opencontainers.image.ref.name":"v0.1.1"},"platform":{"architecture":"amd64","os":"linux"}}]} \ No newline at end of file diff --git a/oci/layout/fixtures/rejected_certs/cert.cert b/oci/layout/fixtures/rejected_certs/cert.cert new file mode 100644 index 0000000000..4b4f0a9cc6 --- /dev/null +++ b/oci/layout/fixtures/rejected_certs/cert.cert @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICDDCCAW2gAwIBAgIRAK2Kd9uMIa+8QMULJEerBfkwCgYIKoZIzj0EAwQwEjEQ +MA4GA1UEChMHQWNtZSBDbzAeFw0xNzA5MjIxMjE2NThaFw0xODA5MjIxMjE2NTha +MBIxEDAOBgNVBAoTB0FjbWUgQ28wgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAFH +j4kX+5EWC7oorajgfWJHDs9Tx2H2VjvzzLruJnmzc6+TVMKDXoaqxByvxUJ6HXEV +VthYgjemoGF32yWbzV+D5wGTlh37IKWOpHVKGLn6vQ7kFaFUTl65l3IGzpGd+CDm +xrT2GYVdLjmrRINNby5JC7flsCqv6FPlbD/qA9S8a3U5r6NhMF8wDgYDVR0PAQH/ +BAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdEQQWMBSCCWxvY2FsaG9zdIEHYkBiLmNvbTAKBggqhkjOPQQD +BAOBjAAwgYgCQgGEJ3gBvWcs3KOYi3akz5H/QY8+NROGTRZ8VHRmySgPNKiUq9by +5kxb9LhwnzIHhcYDQS7jJCaZpzzD+rkYrG9MEAJCAcsyzixyufE8iOrKc1qHKnya +5poyGv5+WoloVozs8NreR7ZhYsOwPHXt10ciGPDN+7EZnzVvYI+e1ZMLz1NtF0Gr +-----END CERTIFICATE----- diff --git a/oci/layout/fixtures/rejected_certs/cert.key b/oci/layout/fixtures/rejected_certs/cert.key new file mode 100644 index 0000000000..e68f355d91 --- /dev/null +++ b/oci/layout/fixtures/rejected_certs/cert.key @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIB3BPUEOohwxGCV8V2fwIBdZ3S7yWADrbz5w17YITBt0p6j1C0NKRx +xL9V7Cq+P2OkfQa6rxiD7cM8DjP/6y1//XKgBwYFK4EEACOhgYkDgYYABAFHj4kX ++5EWC7oorajgfWJHDs9Tx2H2VjvzzLruJnmzc6+TVMKDXoaqxByvxUJ6HXEVVthY +gjemoGF32yWbzV+D5wGTlh37IKWOpHVKGLn6vQ7kFaFUTl65l3IGzpGd+CDmxrT2 +GYVdLjmrRINNby5JC7flsCqv6FPlbD/qA9S8a3U5rw== +-----END EC PRIVATE KEY----- diff --git a/oci/layout/oci_dest.go b/oci/layout/oci_dest.go index c4801e34fa..ce1e0c3e2b 100644 --- a/oci/layout/oci_dest.go +++ b/oci/layout/oci_dest.go @@ -66,7 +66,7 @@ func (d *ociImageDestination) ShouldCompressLayers() bool { // AcceptsForeignLayerURLs returns false iff foreign layers in manifest should be actually // uploaded to the image destination, true otherwise. func (d *ociImageDestination) AcceptsForeignLayerURLs() bool { - return false + return true } // MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime OS. False otherwise. diff --git a/oci/layout/oci_src.go b/oci/layout/oci_src.go index 99b9f2083a..be8a2aa734 100644 --- a/oci/layout/oci_src.go +++ b/oci/layout/oci_src.go @@ -4,25 +4,43 @@ import ( "context" "io" "io/ioutil" + "net/http" "os" + "strconv" + "github.com/containers/image/pkg/tlsclientconfig" "github.com/containers/image/types" + "github.com/docker/go-connections/tlsconfig" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" ) type ociImageSource struct { ref ociReference descriptor imgspecv1.Descriptor + client *http.Client } // newImageSource returns an ImageSource for reading from an existing directory. -func newImageSource(ref ociReference) (types.ImageSource, error) { +func newImageSource(ctx *types.SystemContext, ref ociReference) (types.ImageSource, error) { + tr := tlsclientconfig.NewTransport() + tr.TLSClientConfig = tlsconfig.ServerDefault() + + if ctx != nil && ctx.OCICertPath != "" { + if err := tlsclientconfig.SetupCertificates(ctx.OCICertPath, tr.TLSClientConfig); err != nil { + return nil, err + } + tr.TLSClientConfig.InsecureSkipVerify = ctx.OCIInsecureSkipTLSVerify + } + + client := &http.Client{} + client.Transport = tr descriptor, err := ref.getManifestDescriptor() if err != nil { return nil, err } - return &ociImageSource{ref: ref, descriptor: descriptor}, nil + return &ociImageSource{ref: ref, descriptor: descriptor, client: client}, nil } // Reference returns the reference used to set up this source. @@ -70,6 +88,10 @@ func (s *ociImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string // GetBlob returns a stream for the specified blob, and the blob's size. func (s *ociImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) { + if len(info.URLs) != 0 { + return s.getExternalBlob(info.URLs) + } + path, err := s.ref.blobPath(info.Digest) if err != nil { return nil, 0, err @@ -89,3 +111,32 @@ func (s *ociImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, err func (s *ociImageSource) GetSignatures(context.Context) ([][]byte, error) { return [][]byte{}, nil } + +func (s *ociImageSource) getExternalBlob(urls []string) (io.ReadCloser, int64, error) { + errWrap := errors.New("failed fetching external blob from all urls") + for _, url := range urls { + resp, err := s.client.Get(url) + if err != nil { + errWrap = errors.Wrapf(errWrap, "fetching %s failed %s", url, err.Error()) + continue + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + errWrap = errors.Wrapf(errWrap, "fetching %s failed, response code not 200", url) + continue + } + + return resp.Body, getBlobSize(resp), nil + } + + return nil, 0, errWrap +} + +func getBlobSize(resp *http.Response) int64 { + size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) + if err != nil { + size = -1 + } + return size +} diff --git a/oci/layout/oci_src_test.go b/oci/layout/oci_src_test.go new file mode 100644 index 0000000000..8a54ffd130 --- /dev/null +++ b/oci/layout/oci_src_test.go @@ -0,0 +1,132 @@ +package layout + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/containers/image/types" + digest "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const RemoteLayerContent = "This is the remote layer content" + +var httpServerAddr string + +func TestMain(m *testing.M) { + httpServer, err := startRemoteLayerServer() + if err != nil { + println("Error starting test TLS server", err.Error()) + os.Exit(1) + } + + httpServerAddr = strings.Replace(httpServer.URL, "127.0.0.1", "localhost", 1) + code := m.Run() + httpServer.Close() + os.Exit(code) +} + +func TestGetBlobForRemoteLayers(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello world") + })) + defer ts.Close() + + imageSource := createImageSource(t, &types.SystemContext{}) + layerInfo := types.BlobInfo{ + Digest: digest.FromBytes([]byte("Hello world")), + Size: -1, + URLs: []string{ + "brokenurl", + ts.URL, + }, + } + + reader, _, err := imageSource.GetBlob(layerInfo) + require.NoError(t, err) + defer reader.Close() + + data, err := ioutil.ReadAll(reader) + require.NoError(t, err) + assert.Contains(t, string(data), "Hello world") +} + +func TestGetBlobForRemoteLayersWithTLS(t *testing.T) { + imageSource := createImageSource(t, &types.SystemContext{ + OCICertPath: "fixtures/accepted_certs", + }) + + layer, size, err := imageSource.GetBlob(types.BlobInfo{ + URLs: []string{httpServerAddr}, + }) + require.NoError(t, err) + + layerContent, _ := ioutil.ReadAll(layer) + assert.Equal(t, RemoteLayerContent, string(layerContent)) + assert.Equal(t, int64(len(RemoteLayerContent)), size) +} + +func TestGetBlobForRemoteLayersOnTLSFailure(t *testing.T) { + imageSource := createImageSource(t, &types.SystemContext{ + OCICertPath: "fixtures/rejected_certs", + }) + layer, size, err := imageSource.GetBlob(types.BlobInfo{ + URLs: []string{httpServerAddr}, + }) + + require.Error(t, err) + assert.Nil(t, layer) + assert.Equal(t, int64(0), size) +} + +func remoteLayerContent(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, RemoteLayerContent) +} + +func startRemoteLayerServer() (*httptest.Server, error) { + certBytes, err := ioutil.ReadFile("fixtures/accepted_certs/cert.cert") + if err != nil { + return nil, err + } + + clientCertPool := x509.NewCertPool() + if !clientCertPool.AppendCertsFromPEM(certBytes) { + return nil, fmt.Errorf("Could not append certificate") + } + + cert, err := tls.LoadX509KeyPair("fixtures/accepted_certs/cert.cert", "fixtures/accepted_certs/cert.key") + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + // Reject any TLS certificate that cannot be validated + ClientAuth: tls.RequireAndVerifyClientCert, + // Ensure that we only use our "CA" to validate certificates + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{cert}, + } + + httpServer := httptest.NewUnstartedServer(http.HandlerFunc(remoteLayerContent)) + httpServer.TLS = tlsConfig + + httpServer.StartTLS() + + return httpServer, nil +} + +func createImageSource(t *testing.T, context *types.SystemContext) types.ImageSource { + imageRef, err := NewReference("fixtures/manifest", "") + require.NoError(t, err) + imageSource, err := imageRef.NewImageSource(context) + require.NoError(t, err) + return imageSource +} diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go index 7fd826f476..312bc0e4eb 100644 --- a/oci/layout/oci_transport.go +++ b/oci/layout/oci_transport.go @@ -182,7 +182,7 @@ func (ref ociReference) PolicyConfigurationNamespaces() []string { // NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource, // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage. func (ref ociReference) NewImage(ctx *types.SystemContext) (types.Image, error) { - src, err := newImageSource(ref) + src, err := newImageSource(ctx, ref) if err != nil { return nil, err } @@ -244,7 +244,7 @@ func LoadManifestDescriptor(imgRef types.ImageReference) (imgspecv1.Descriptor, // NewImageSource returns a types.ImageSource for this reference. // The caller must call .Close() on the returned ImageSource. func (ref ociReference) NewImageSource(ctx *types.SystemContext) (types.ImageSource, error) { - return newImageSource(ref) + return newImageSource(ctx, ref) } // NewImageDestination returns a types.ImageDestination for this reference. diff --git a/pkg/tlsclientconfig/tlsclientconfig.go b/pkg/tlsclientconfig/tlsclientconfig.go new file mode 100644 index 0000000000..0a32861ced --- /dev/null +++ b/pkg/tlsclientconfig/tlsclientconfig.go @@ -0,0 +1,102 @@ +package tlsclientconfig + +import ( + "crypto/tls" + "io/ioutil" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/docker/go-connections/sockets" + "github.com/docker/go-connections/tlsconfig" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// SetupCertificates opens all .crt, .cert, and .key files in dir and appends / loads certs and key pairs as appropriate to tlsc +func SetupCertificates(dir string, tlsc *tls.Config) error { + logrus.Debugf("Looking for TLS certificates and private keys in %s", dir) + fs, err := ioutil.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + if os.IsPermission(err) { + logrus.Debugf("Skipping scan of %s due to permission error: %v", dir, err) + return nil + } + return err + } + + for _, f := range fs { + fullPath := filepath.Join(dir, f.Name()) + if strings.HasSuffix(f.Name(), ".crt") { + systemPool, err := tlsconfig.SystemCertPool() + if err != nil { + return errors.Wrap(err, "unable to get system cert pool") + } + tlsc.RootCAs = systemPool + logrus.Debugf(" crt: %s", fullPath) + data, err := ioutil.ReadFile(fullPath) + if err != nil { + return err + } + tlsc.RootCAs.AppendCertsFromPEM(data) + } + if strings.HasSuffix(f.Name(), ".cert") { + certName := f.Name() + keyName := certName[:len(certName)-5] + ".key" + logrus.Debugf(" cert: %s", fullPath) + if !hasFile(fs, keyName) { + return errors.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName) + } + cert, err := tls.LoadX509KeyPair(filepath.Join(dir, certName), filepath.Join(dir, keyName)) + if err != nil { + return err + } + tlsc.Certificates = append(tlsc.Certificates, cert) + } + if strings.HasSuffix(f.Name(), ".key") { + keyName := f.Name() + certName := keyName[:len(keyName)-4] + ".cert" + logrus.Debugf(" key: %s", fullPath) + if !hasFile(fs, certName) { + return errors.Errorf("missing client certificate %s for key %s", certName, keyName) + } + } + } + return nil +} + +func hasFile(files []os.FileInfo, name string) bool { + for _, f := range files { + if f.Name() == name { + return true + } + } + return false +} + +// NewTransport Creates a default transport +func NewTransport() *http.Transport { + direct := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + } + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: direct.Dial, + TLSHandshakeTimeout: 10 * time.Second, + // TODO(dmcgowan): Call close idle connections when complete and use keep alive + DisableKeepAlives: true, + } + proxyDialer, err := sockets.DialerFromEnvironment(direct) + if err == nil { + tr.Dial = proxyDialer.Dial + } + return tr +} diff --git a/types/types.go b/types/types.go index a042410667..e955c33806 100644 --- a/types/types.go +++ b/types/types.go @@ -305,6 +305,14 @@ type SystemContext struct { // Path to the system-wide registries configuration file SystemRegistriesConfPath string + // === OCI.Transport overrides === + // If not "", a directory containing a CA certificate (ending with ".crt"), + // a client certificate (ending with ".cert") and a client ceritificate key + // (ending with ".key") used when downloading OCI image layers. + OCICertPath string + // Allow downloading OCI image layers over HTTP, or HTTPS with failed TLS verification. Note that this does not affect other TLS connections. + OCIInsecureSkipTLSVerify bool + // === docker.Transport overrides === // If not "", a directory containing a CA certificate (ending with ".crt"), // a client certificate (ending with ".cert") and a client ceritificate key