diff --git a/changelog/0.8.2/issue-1522 b/changelog/0.8.2/issue-1522 index 813b0b108..57e0501c3 100644 --- a/changelog/0.8.2/issue-1522 +++ b/changelog/0.8.2/issue-1522 @@ -1,9 +1,8 @@ -Enhancement: Add support for TLS client certificate authentication. +Enhancement: Add support for TLS client certificate authentication -Support has been added to the http backend for providing a TLS client -certificate/key pair using `--tls-client-cert` and `--tls-client-key` when -connecting to https resources that perform TLS client certificate -authentication. +Support has been added for using a TLS client certificate for authentication to +HTTP based backend. A file containing the PEM encoded private key and +certificate can be set using the `--tls-client-cert` option. https://github.com/restic/restic/issues/1522 https://github.com/restic/restic/pull/1524 diff --git a/cmd/restic/global.go b/cmd/restic/global.go index adfae4b94..fc916cbef 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -48,7 +48,6 @@ type GlobalOptions struct { NoCache bool CACerts []string TLSClientCert string - TLSClientKey string CleanupCache bool LimitUploadKb int @@ -86,8 +85,7 @@ func init() { f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory") f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache") f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "path to load root certificates from (default: use system certificates)") - f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a TLS client certificate") - f.StringVar(&globalOptions.TLSClientKey, "tls-client-key", "", "path to a TLS client certificate key") + f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a file containing PEM encoded TLS client certificate and private key") f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories") f.IntVar(&globalOptions.LimitUploadKb, "limit-upload", 0, "limits uploads to a maximum rate in KiB/s. (default: unlimited)") f.IntVar(&globalOptions.LimitDownloadKb, "limit-download", 0, "limits downloads to a maximum rate in KiB/s. (default: unlimited)") @@ -545,7 +543,11 @@ func open(s string, gopts GlobalOptions, opts options.Options) (restic.Backend, return nil, err } - rt, err := backend.Transport(globalOptions.CACerts, globalOptions.TLSClientCert, globalOptions.TLSClientKey) + tropts := backend.TransportOptions{ + RootCertFilenames: globalOptions.CACerts, + TLSClientCertKeyFilename: globalOptions.TLSClientCert, + } + rt, err := backend.Transport(tropts) if err != nil { return nil, err } @@ -609,7 +611,11 @@ func create(s string, opts options.Options) (restic.Backend, error) { return nil, err } - rt, err := backend.Transport(globalOptions.CACerts, globalOptions.TLSClientCert, globalOptions.TLSClientKey) + tropts := backend.TransportOptions{ + RootCertFilenames: globalOptions.CACerts, + TLSClientCertKeyFilename: globalOptions.TLSClientCert, + } + rt, err := backend.Transport(tropts) if err != nil { return nil, err } diff --git a/doc/manual_rest.rst b/doc/manual_rest.rst index e203b6e30..821f220fd 100644 --- a/doc/manual_rest.rst +++ b/doc/manual_rest.rst @@ -51,8 +51,8 @@ Usage help is available: -p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE) -q, --quiet do not output comprehensive progress report -r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY) - --tls-client-cert string path to a TLS client certificate - --tls-client-key string path to a TLS client certificate key + --tls-client-cert string path to a file containing PEM encoded TLS client certificate and private key + Use "restic [command] --help" for more information about a command. diff --git a/internal/backend/azure/azure_test.go b/internal/backend/azure/azure_test.go index c6c61dfc1..d738de857 100644 --- a/internal/backend/azure/azure_test.go +++ b/internal/backend/azure/azure_test.go @@ -16,7 +16,7 @@ import ( ) func newAzureTestSuite(t testing.TB) *test.Suite { - tr, err := backend.Transport(nil, "", "") + tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { t.Fatalf("cannot create transport for tests: %v", err) } diff --git a/internal/backend/b2/b2_test.go b/internal/backend/b2/b2_test.go index e95f7dd42..9f97de4f9 100644 --- a/internal/backend/b2/b2_test.go +++ b/internal/backend/b2/b2_test.go @@ -16,7 +16,7 @@ import ( ) func newB2TestSuite(t testing.TB) *test.Suite { - tr, err := backend.Transport(nil, "", "") + tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { t.Fatalf("cannot create transport for tests: %v", err) } diff --git a/internal/backend/http_transport.go b/internal/backend/http_transport.go index 71c4e0501..324874c63 100644 --- a/internal/backend/http_transport.go +++ b/internal/backend/http_transport.go @@ -3,19 +3,66 @@ package backend import ( "crypto/tls" "crypto/x509" - "fmt" + "encoding/pem" "io/ioutil" "net" "net/http" + "os" + "strings" "time" "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" ) +// TransportOptions collects various options which can be set for an HTTP based +// transport. +type TransportOptions struct { + // contains filenames of PEM encoded root certificates to trust + RootCertFilenames []string + + // contains the name of a file containing the TLS client certificate and private key in PEM format + TLSClientCertKeyFilename string +} + +// readPEMCertKey reads a file and returns the PEM encoded certificate and key +// blocks. +func readPEMCertKey(filename string) (certs []byte, key []byte, err error) { + data, err := ioutil.ReadFile(os.Args[1]) + if err != nil { + return nil, nil, errors.Wrap(err, "ReadFile") + } + + var block *pem.Block + for { + if len(data) == 0 { + break + } + block, data = pem.Decode(data) + if block == nil { + break + } + + switch { + case strings.HasSuffix(block.Type, "CERTIFICATE"): + certs = append(certs, pem.EncodeToMemory(block)...) + case strings.HasSuffix(block.Type, "PRIVATE KEY"): + if key != nil { + return nil, nil, errors.Errorf("error loading TLS cert and key from %v: more than one private key found", filename) + } + key = pem.EncodeToMemory(block) + default: + return nil, nil, errors.Errorf("error loading TLS cert and key from %v: unknown block type %v found", filename, block.Type) + } + } + + return certs, key, nil +} + // Transport returns a new http.RoundTripper with default settings applied. If // a custom rootCertFilename is non-empty, it must point to a valid PEM file, // otherwise the function will return an error. -func Transport(rootCertFilenames []string, tlsClientCert string, tlsClientKey string) (http.RoundTripper, error) { +func Transport(opts TransportOptions) (http.RoundTripper, error) { // copied from net/http tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, @@ -32,34 +79,36 @@ func Transport(rootCertFilenames []string, tlsClientCert string, tlsClientKey st TLSClientConfig: &tls.Config{}, } - if tlsClientCert != "" && tlsClientKey != "" { - c, err := tls.LoadX509KeyPair(tlsClientCert, tlsClientKey) + if opts.TLSClientCertKeyFilename != "" { + certs, key, err := readPEMCertKey(opts.TLSClientCertKeyFilename) if err != nil { - return nil, fmt.Errorf("unable to read client certificate/key pair: %v", err) + return nil, err } - tr.TLSClientConfig.Certificates = []tls.Certificate{c} - } - if rootCertFilenames == nil { - return debug.RoundTripper(tr), nil - } - - p := x509.NewCertPool() - for _, filename := range rootCertFilenames { - if filename == "" { - return nil, fmt.Errorf("empty filename for root certificate supplied") - } - b, err := ioutil.ReadFile(filename) + crt, err := tls.X509KeyPair(certs, key) if err != nil { - return nil, fmt.Errorf("unable to read root certificate: %v", err) - } - if ok := p.AppendCertsFromPEM(b); !ok { - return nil, fmt.Errorf("cannot parse root certificate from %q", filename) + return nil, errors.Errorf("parse TLS client cert or key: %v", err) } + tr.TLSClientConfig.Certificates = []tls.Certificate{crt} } - tr.TLSClientConfig.RootCAs = p + if opts.RootCertFilenames != nil { + pool := x509.NewCertPool() + for _, filename := range opts.RootCertFilenames { + if filename == "" { + return nil, errors.Errorf("empty filename for root certificate supplied") + } + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errors.Errorf("unable to read root certificate: %v", err) + } + if ok := pool.AppendCertsFromPEM(b); !ok { + return nil, errors.Errorf("cannot parse root certificate from %q", filename) + } + } + tr.TLSClientConfig.RootCAs = pool + } - // wrap in the debug round tripper + // wrap in the debug round tripper (if active) return debug.RoundTripper(tr), nil } diff --git a/internal/backend/rest/rest_test.go b/internal/backend/rest/rest_test.go index 487d1b413..59dde38ee 100644 --- a/internal/backend/rest/rest_test.go +++ b/internal/backend/rest/rest_test.go @@ -68,7 +68,7 @@ func runRESTServer(ctx context.Context, t testing.TB, dir string) (*url.URL, fun } func newTestSuite(ctx context.Context, t testing.TB, url *url.URL, minimalData bool) *test.Suite { - tr, err := backend.Transport(nil, "", "") + tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { t.Fatalf("cannot create transport for tests: %v", err) } diff --git a/internal/backend/s3/s3_test.go b/internal/backend/s3/s3_test.go index 0a5ae48e4..35a80bf7e 100644 --- a/internal/backend/s3/s3_test.go +++ b/internal/backend/s3/s3_test.go @@ -121,7 +121,7 @@ func createS3(t testing.TB, cfg MinioTestConfig, tr http.RoundTripper) (be resti } func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite { - tr, err := backend.Transport(nil, "", "") + tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { t.Fatalf("cannot create transport for tests: %v", err) } @@ -221,7 +221,7 @@ func BenchmarkBackendMinio(t *testing.B) { } func newS3TestSuite(t testing.TB) *test.Suite { - tr, err := backend.Transport(nil, "", "") + tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { t.Fatalf("cannot create transport for tests: %v", err) } diff --git a/internal/backend/swift/swift_test.go b/internal/backend/swift/swift_test.go index 8e81064f9..2c4781554 100644 --- a/internal/backend/swift/swift_test.go +++ b/internal/backend/swift/swift_test.go @@ -16,7 +16,7 @@ import ( ) func newSwiftTestSuite(t testing.TB) *test.Suite { - tr, err := backend.Transport(nil, "", "") + tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { t.Fatalf("cannot create transport for tests: %v", err) }