/*
 *
 * Copyright 2021 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package xds

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"
	"net/netip"
	"net/url"
	"os"
	"regexp"
	"strings"
	"testing"
	"time"

	"google.golang.org/grpc/testdata"

	"google.golang.org/grpc/credentials/tls/certprovider"
	"google.golang.org/grpc/internal/credentials/spiffe"
	"google.golang.org/grpc/internal/envconfig"
	"google.golang.org/grpc/internal/grpctest"
	"google.golang.org/grpc/internal/testutils"
	"google.golang.org/grpc/internal/xds/matcher"
)

type s struct {
	grpctest.Tester
}

func Test(t *testing.T) {
	grpctest.RunSubTests(t, s{})
}

type testCertProvider struct {
	certprovider.Provider
}

type testCertProviderWithKeyMaterial struct {
	certprovider.Provider
}

func (s) TestDNSMatch(t *testing.T) {
	tests := []struct {
		desc      string
		host      string
		pattern   string
		wantMatch bool
	}{
		{
			desc:      "invalid wildcard 1",
			host:      "aa.example.com",
			pattern:   "*a.example.com",
			wantMatch: false,
		},
		{
			desc:      "invalid wildcard 2",
			host:      "aa.example.com",
			pattern:   "a*.example.com",
			wantMatch: false,
		},
		{
			desc:      "invalid wildcard 3",
			host:      "abc.example.com",
			pattern:   "a*c.example.com",
			wantMatch: false,
		},
		{
			desc:      "wildcard in one of the middle components",
			host:      "abc.test.example.com",
			pattern:   "abc.*.example.com",
			wantMatch: false,
		},
		{
			desc:      "single component wildcard",
			host:      "a.example.com",
			pattern:   "*",
			wantMatch: false,
		},
		{
			desc:      "short host name",
			host:      "a.com",
			pattern:   "*.example.com",
			wantMatch: false,
		},
		{
			desc:      "suffix mismatch",
			host:      "a.notexample.com",
			pattern:   "*.example.com",
			wantMatch: false,
		},
		{
			desc:      "wildcard match across components",
			host:      "sub.test.example.com",
			pattern:   "*.example.com.",
			wantMatch: false,
		},
		{
			desc:      "host doesn't end in period",
			host:      "test.example.com",
			pattern:   "test.example.com.",
			wantMatch: true,
		},
		{
			desc:      "pattern doesn't end in period",
			host:      "test.example.com.",
			pattern:   "test.example.com",
			wantMatch: true,
		},
		{
			desc:      "case insensitive",
			host:      "TEST.EXAMPLE.COM.",
			pattern:   "test.example.com.",
			wantMatch: true,
		},
		{
			desc:      "simple match",
			host:      "test.example.com",
			pattern:   "test.example.com",
			wantMatch: true,
		},
		{
			desc:      "good wildcard",
			host:      "a.example.com",
			pattern:   "*.example.com",
			wantMatch: true,
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			gotMatch := dnsMatch(test.host, test.pattern)
			if gotMatch != test.wantMatch {
				t.Fatalf("dnsMatch(%s, %s) = %v, want %v", test.host, test.pattern, gotMatch, test.wantMatch)
			}
		})
	}
}

func (s) TestMatchingSANExists_FailureCases(t *testing.T) {
	url1, err := url.Parse("http://golang.org")
	if err != nil {
		t.Fatalf("url.Parse() failed: %v", err)
	}
	url2, err := url.Parse("https://github.com/grpc/grpc-go")
	if err != nil {
		t.Fatalf("url.Parse() failed: %v", err)
	}
	inputCert := &x509.Certificate{
		DNSNames:       []string{"foo.bar.example.com", "bar.baz.test.com", "*.example.com"},
		EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"},
		IPAddresses: []net.IP{
			netip.MustParseAddr("192.0.0.1").AsSlice(),
			netip.MustParseAddr("2001:db8::68").AsSlice(),
		},
		URIs: []*url.URL{url1, url2},
	}

	tests := []struct {
		desc        string
		sanMatchers []matcher.StringMatcher
	}{
		{
			desc: "exact match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewExactStringMatcher("abcd.test.com", false),
				matcher.NewExactStringMatcher("http://golang", false),
				matcher.NewExactStringMatcher("HTTP://GOLANG.ORG", false),
			},
		},
		{
			desc: "prefix match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewPrefixStringMatcher("i-aint-the-one", false),
				matcher.NewPrefixStringMatcher("192.168.1.1", false),
				matcher.NewPrefixStringMatcher("FOO.BAR", false),
			},
		},
		{
			desc: "suffix match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewSuffixStringMatcher("i-aint-the-one", false),
				matcher.NewSuffixStringMatcher("1::68", false),
				matcher.NewSuffixStringMatcher(".COM", false),
			},
		},
		{
			desc: "regex match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewRegexStringMatcher(regexp.MustCompile(`.*\.examples\.com`)),
				matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)),
			},
		},
		{
			desc: "contains match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewContainsStringMatcher("i-aint-the-one", false),
				matcher.NewContainsStringMatcher("2001:db8:1:1::68", false),
				matcher.NewContainsStringMatcher("GRPC", false),
			},
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			hi := NewHandshakeInfo(nil, nil, test.sanMatchers, false, "", false, false)

			if hi.MatchingSANExists(inputCert) {
				t.Fatalf("hi.MatchingSANExists(%+v) with SAN matchers +%v succeeded when expected to fail", inputCert, test.sanMatchers)
			}
		})
	}
}

func (s) TestMatchingSANExists_Success(t *testing.T) {
	url1, err := url.Parse("http://golang.org")
	if err != nil {
		t.Fatalf("url.Parse() failed: %v", err)
	}
	url2, err := url.Parse("https://github.com/grpc/grpc-go")
	if err != nil {
		t.Fatalf("url.Parse() failed: %v", err)
	}
	inputCert := &x509.Certificate{
		DNSNames:       []string{"baz.test.com", "*.example.com"},
		EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"},
		IPAddresses: []net.IP{
			netip.MustParseAddr("192.0.0.1").AsSlice(),
			netip.MustParseAddr("2001:db8::68").AsSlice(),
		},
		URIs: []*url.URL{url1, url2},
	}

	tests := []struct {
		desc        string
		sanMatchers []matcher.StringMatcher
	}{
		{
			desc: "no san matchers",
		},
		{
			desc: "exact match dns wildcard",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewPrefixStringMatcher("192.168.1.1", false),
				matcher.NewExactStringMatcher("https://github.com/grpc/grpc-java", false),
				matcher.NewExactStringMatcher("abc.example.com", false),
			},
		},
		{
			desc: "exact match ignore case",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewExactStringMatcher("FOOBAR@EXAMPLE.COM", true),
			},
		},
		{
			desc: "prefix match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewSuffixStringMatcher(".co.in", false),
				matcher.NewPrefixStringMatcher("192.168.1.1", false),
				matcher.NewPrefixStringMatcher("baz.test", false),
			},
		},
		{
			desc: "prefix match ignore case",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewPrefixStringMatcher("BAZ.test", true),
			},
		},
		{
			desc: "suffix  match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)),
				matcher.NewSuffixStringMatcher("192.168.1.1", false),
				matcher.NewSuffixStringMatcher("@test.com", false),
			},
		},
		{
			desc: "suffix  match ignore case",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewSuffixStringMatcher("@test.COM", true),
			},
		},
		{
			desc: "regex match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewContainsStringMatcher("https://github.com/grpc/grpc-java", false),
				matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)),
				matcher.NewRegexStringMatcher(regexp.MustCompile(`.*\.test\.com`)),
			},
		},
		{
			desc: "contains match",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewExactStringMatcher("https://github.com/grpc/grpc-java", false),
				matcher.NewContainsStringMatcher("2001:68::db8", false),
				matcher.NewContainsStringMatcher("192.0.0", false),
			},
		},
		{
			desc: "contains match ignore case",
			sanMatchers: []matcher.StringMatcher{
				matcher.NewContainsStringMatcher("GRPC", true),
			},
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			hi := NewHandshakeInfo(nil, nil, test.sanMatchers, false, "", false, false)

			if !hi.MatchingSANExists(inputCert) {
				t.Fatalf("hi.MatchingSANExists(%+v) with SAN matchers +%v failed when expected to succeed", inputCert, test.sanMatchers)
			}
		})
	}
}

func (s) TestEqual(t *testing.T) {
	tests := []struct {
		desc      string
		hi1       *HandshakeInfo
		hi2       *HandshakeInfo
		wantMatch bool
	}{
		{
			desc:      "both HandshakeInfo are nil",
			hi1:       nil,
			hi2:       nil,
			wantMatch: true,
		},
		{
			desc:      "one HandshakeInfo is nil",
			hi1:       nil,
			hi2:       NewHandshakeInfo(&testCertProvider{}, nil, nil, false, "", false, false),
			wantMatch: false,
		},
		{
			desc:      "different root providers",
			hi1:       NewHandshakeInfo(&testCertProvider{}, nil, nil, false, "", false, false),
			hi2:       NewHandshakeInfo(&testCertProvider{}, nil, nil, false, "", false, false),
			wantMatch: false,
		},
		{
			desc: "same providers, same SAN matchers",
			hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
				matcher.NewExactStringMatcher("foo.com", false),
			}, false, "", false, false),
			hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
				matcher.NewExactStringMatcher("foo.com", false),
			}, false, "", false, false),
			wantMatch: true,
		},
		{
			desc: "same providers, different SAN matchers",
			hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
				matcher.NewExactStringMatcher("foo.com", false),
			}, false, "", false, false),
			hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
				matcher.NewExactStringMatcher("bar.com", false),
			}, false, "", false, false),
			wantMatch: false,
		},
		{
			desc: "same SAN matchers with different content",
			hi1: NewHandshakeInfo(&testCertProvider{}, &testCertProvider{}, []matcher.StringMatcher{
				matcher.NewExactStringMatcher("foo.com", false),
			}, false, "", false, false),
			hi2: NewHandshakeInfo(&testCertProvider{}, &testCertProvider{}, []matcher.StringMatcher{
				matcher.NewExactStringMatcher("foo.com", false),
				matcher.NewExactStringMatcher("bar.com", false),
			}, false, "", false, false),
			wantMatch: false,
		},
		{
			desc:      "different requireClientCert flags",
			hi1:       NewHandshakeInfo(&testCertProvider{}, &testCertProvider{}, nil, true, "", false, false),
			hi2:       NewHandshakeInfo(&testCertProvider{}, &testCertProvider{}, nil, false, "", false, false),
			wantMatch: false,
		},
		{
			desc:      "same identity provider, different root provider",
			hi1:       NewHandshakeInfo(&testCertProvider{}, testCertProvider{}, nil, false, "", false, false),
			hi2:       NewHandshakeInfo(&testCertProvider{}, testCertProvider{}, nil, false, "", false, false),
			wantMatch: false,
		},
		{
			desc:      "different identity provider, same root provider",
			hi1:       NewHandshakeInfo(testCertProvider{}, &testCertProvider{}, nil, false, "", false, false),
			hi2:       NewHandshakeInfo(testCertProvider{}, &testCertProvider{}, nil, false, "", false, false),
			wantMatch: false,
		},
		{
			desc:      "same sni and validateSANUsingSNI",
			hi1:       NewHandshakeInfo(nil, nil, nil, false, "sni", true, false),
			hi2:       NewHandshakeInfo(nil, nil, nil, false, "sni", true, false),
			wantMatch: true,
		},
		{
			desc:      "different sni",
			hi1:       NewHandshakeInfo(nil, nil, nil, false, "sni1", false, false),
			hi2:       NewHandshakeInfo(nil, nil, nil, false, "sni2", false, false),
			wantMatch: false,
		},
		{
			desc:      "same sni, different validateSANUsingSNI",
			hi1:       NewHandshakeInfo(nil, nil, nil, false, "sni", false, false),
			hi2:       NewHandshakeInfo(nil, nil, nil, false, "sni", true, false),
			wantMatch: false,
		},
		{
			desc:      "different autoHostSNI",
			hi1:       NewHandshakeInfo(nil, nil, nil, false, "", false, true),
			hi2:       NewHandshakeInfo(nil, nil, nil, false, "", false, false),
			wantMatch: false,
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			if gotMatch := test.hi1.Equal(test.hi2); gotMatch != test.wantMatch {
				t.Errorf("hi1.Equal(hi2) = %v; wantMatch %v", gotMatch, test.wantMatch)
			}
		})
	}
}

func (p *testCertProviderWithKeyMaterial) KeyMaterial(_ context.Context) (*certprovider.KeyMaterial, error) {
	km := &certprovider.KeyMaterial{}
	spiffeBundleMapContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client_spiffebundle.json"))
	if err != nil {
		return nil, err
	}
	bundleMap, err := spiffe.BundleMapFromBytes(spiffeBundleMapContents)
	if err != nil {
		return nil, err
	}
	km.SPIFFEBundleMap = bundleMap
	rootFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/ca.pem"))
	if err != nil {
		return nil, err
	}
	trustPool := x509.NewCertPool()
	if !trustPool.AppendCertsFromPEM(rootFileContents) {
		return nil, fmt.Errorf("Failed to parse root certificate")
	}
	km.Roots = trustPool

	certFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client_spiffe.pem"))
	if err != nil {
		return nil, err
	}
	keyFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client.key"))
	if err != nil {
		return nil, err
	}
	cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
	if err != nil {
		return nil, err
	}
	km.Certs = []tls.Certificate{cert}
	return km, nil
}

func (s) TestBuildVerifyFuncFailures(t *testing.T) {
	tests := []struct {
		desc          string
		peerCertChain [][]byte
		wantErr       string
	}{
		{
			desc:          "invalid x509",
			peerCertChain: [][]byte{[]byte("NOT_A_CERT")},
			wantErr:       "x509: malformed certificate",
		},
		{
			desc: "invalid SPIFFE ID in peer cert",
			// server1.pem doesn't have a valid SPIFFE ID, so attempted to get a
			// root from the SPIFFE Bundle Map will fail
			peerCertChain: loadCert(t, testdata.Path("server1.pem"), testdata.Path("server1.key")),
			wantErr:       "spiffe: could not get spiffe ID from peer leaf cert but verification with spiffe trust map was configure",
		},
	}
	testProvider := testCertProviderWithKeyMaterial{}
	hi := NewHandshakeInfo(&testProvider, &testProvider, nil, true, "", false, false)
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	cfg, err := hi.ClientSideTLSConfig(ctx, "")
	if err != nil {
		t.Fatalf("hi.ClientSideTLSConfig() failed with err %v", err)
	}
	for _, tc := range tests {
		t.Run(tc.desc, func(t *testing.T) {
			err = cfg.VerifyPeerCertificate(tc.peerCertChain, nil)
			if !strings.Contains(err.Error(), tc.wantErr) {
				t.Errorf("VerifyPeerCertificate got err %v, want: %v", err, tc.wantErr)
			}
		})
	}
}

func loadCert(t *testing.T, certPath, keyPath string) [][]byte {
	cert, err := tls.LoadX509KeyPair(certPath, keyPath)
	if err != nil {
		t.Fatalf("LoadX509KeyPair(%s, %s) failed: %v", certPath, keyPath, err)
	}
	return cert.Certificate

}

type testProviderWithRoots struct {
	certprovider.Provider
	roots *x509.CertPool
}

func (m *testProviderWithRoots) KeyMaterial(context.Context) (*certprovider.KeyMaterial, error) {
	return &certprovider.KeyMaterial{
		Roots: m.roots,
	}, nil
}

// Tests the scenario where the SNI provided by the xDS server is only present
// in the URI SAN of the certificate presented by the server, and not in the DNS
// SAN. It verifies that if the SNI-based SAN validation is turned on, the TLS
// handshake fails due to SNI mismatch.
func (s) TestAutoHostSNI_DNS_SANValidation_Failures(t *testing.T) {
	testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)

	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa.GenerateKey() failed: %v", err)
	}

	testURL, err := url.Parse("http://wrong.sni.domain")
	if err != nil {
		t.Fatalf("url.Parse() failed: %v", err)
	}

	template := &x509.Certificate{
		NotBefore: time.Now(),
		NotAfter:  time.Now().Add(time.Hour),
		DNSNames:  []string{"unrelated.example.com"},
		URIs:      []*url.URL{testURL}, // URI matching SNI
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
	if err != nil {
		t.Fatalf("x509.CreateCertificate() failed: %v", err)
	}

	cert, err := x509.ParseCertificate(derBytes)
	if err != nil {
		t.Fatalf("x509.ParseCertificate() failed: %v", err)
	}

	roots := x509.NewCertPool()
	roots.AddCert(cert)

	provider := &testProviderWithRoots{roots: roots}
	hi := NewHandshakeInfo(provider, nil, nil, true, "wrong.sni.domain", true, false)
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	cfg, err := hi.ClientSideTLSConfig(ctx, "")
	if err != nil {
		t.Fatalf("hi.ClientSideTLSConfig() failed: %v", err)
	}

	err = cfg.VerifyPeerCertificate([][]byte{derBytes}, nil)
	if err == nil {
		t.Fatalf("VerifyPeerCertificate() unexpectedly succeeded when SNI was only in URI SAN")
	}
	if !strings.Contains(err.Error(), "received DNS SANs: [unrelated.example.com] do not match the SNI: wrong.sni.domain") {
		t.Fatalf("VerifyPeerCertificate() failed with %v, want error containing 'do not match the SNI'", err)
	}
}

func (s) TestVerifyPeerCertificateZeroCerts(t *testing.T) {
	provider := &testProviderWithRoots{roots: x509.NewCertPool()}
	hi := NewHandshakeInfo(provider, nil, nil, true, "", false, false)
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	cfg, err := hi.ClientSideTLSConfig(ctx, "")
	if err != nil {
		t.Fatalf("hi.ClientSideTLSConfig() failed: %v", err)
	}

	const wantErr = "no peer certificates presented"
	if err := cfg.VerifyPeerCertificate(nil, nil); err == nil || !strings.Contains(err.Error(), wantErr) {
		t.Errorf("VerifyPeerCertificate(nil, nil) = %v, want error containing %q", err, wantErr)
	}
}
