// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 tlsmeta

import (
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"testing"
	"time"

	"github.com/elastic/go-lookslike"
	"github.com/elastic/go-lookslike/testslike"

	"github.com/stretchr/testify/require"

	"github.com/elastic/beats/v7/heartbeat/look"
	"github.com/elastic/beats/v7/libbeat/common"
)

// Tests for the non-cert fields
func TestAddTLSMetadata(t *testing.T) {
	// We always test with this one cert because addCertificateMetadata
	// is tested in detail elsewhere
	certs := []*x509.Certificate{parseCert(t, elasticCert)}
	certMetadata := common.MapStr{}
	AddCertMetadata(certMetadata, certs)

	scenarios := []struct {
		name      string
		connState tls.ConnectionState
		duration  time.Duration
		expected  common.MapStr
	}{
		{
			"simple TLSv1.1",
			tls.ConnectionState{
				Version:           tls.VersionTLS11,
				HandshakeComplete: true,
				PeerCertificates:  certs,
				CipherSuite:       tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
				ServerName:        "example.net",
			},
			time.Duration(1),
			common.MapStr{
				"established":      true,
				"rtt":              common.MapStr{"handshake": look.RTT(time.Duration(1))},
				"version_protocol": "tls",
				"version":          "1.1",
				"cipher":           "ECDHE-ECDSA-AES-256-CBC-SHA",
			},
		},
		{
			"TLSv1.2 with next_protocol",
			tls.ConnectionState{
				Version:            tls.VersionTLS12,
				HandshakeComplete:  true,
				PeerCertificates:   certs,
				CipherSuite:        tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
				ServerName:         "example.net",
				NegotiatedProtocol: "h2",
			},
			time.Duration(1),
			common.MapStr{
				"established":      true,
				"rtt":              common.MapStr{"handshake": look.RTT(time.Duration(1))},
				"version_protocol": "tls",
				"version":          "1.2",
				"cipher":           "ECDHE-ECDSA-AES-256-CBC-SHA",
				"next_protocol":    "h2",
			},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			// Nest under the TLS namespace to match actual output
			expected := common.MapStr{"tls": s.expected}

			// Always add in the cert metadata since we test that in other test funcs, not here
			expected.DeepUpdate(certMetadata)

			fields := common.MapStr{}
			AddTLSMetadata(fields, s.connState, s.duration)
			require.Equal(t, expected, fields)
		})
	}
}

func TestAddCertMetadata(t *testing.T) {
	cert := parseCert(t, elasticCert)
	chainCert := parseCert(t, elasticChainCert)
	certNotBefore, err := time.Parse(time.RFC3339, "2019-08-16T01:40:25Z")
	require.NoError(t, err)
	certNotAfter, err := time.Parse(time.RFC3339, "2020-07-16T03:15:39Z")
	require.NoError(t, err)

	expectedFields := lookslike.Strict(lookslike.MustCompile(map[string]interface{}{
		"certificate_not_valid_after":  certNotAfter,
		"certificate_not_valid_before": certNotBefore,
		"server": common.MapStr{
			"hash": common.MapStr{
				"sha1":   "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1",
				"sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d",
			},
			"x509": common.MapStr{
				"issuer": common.MapStr{
					"common_name":        "GlobalSign CloudSSL CA - SHA256 - G3",
					"distinguished_name": "CN=GlobalSign CloudSSL CA - SHA256 - G3,O=GlobalSign nv-sa,C=BE",
				},
				"subject": common.MapStr{
					"common_name":        "r2.shared.global.fastly.net",
					"distinguished_name": "CN=r2.shared.global.fastly.net,O=Fastly\\, Inc.,L=San Francisco,ST=California,C=US",
				},
				"not_after":            certNotAfter,
				"not_before":           certNotBefore,
				"serial_number":        "26610543540289562361990401194",
				"signature_algorithm":  "SHA256-RSA",
				"public_key_algorithm": "RSA",
				"public_key_size":      2048,
				"public_key_exponent":  65537,
			},
		},
	}))

	scenarios := []struct {
		name  string
		certs []*x509.Certificate
	}{
		{
			"single cert fields should all be present",
			[]*x509.Certificate{cert},
		},
		{
			"cert chain should still show single cert fields",
			[]*x509.Certificate{cert, chainCert},
		},
	}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			fields := common.MapStr{}
			AddCertMetadata(fields, scenario.certs)
			tls, err := fields.GetValue("tls")
			require.NoError(t, err)
			testslike.Test(t, expectedFields, tls)
		})
	}
}

// TestCertExpirationMetadata exhaustively tests not before / not after calculation.
func TestCertExpirationMetadata(t *testing.T) {
	goodNotBefore := time.Now().Add(-time.Hour)
	goodNotAfter := time.Now().Add(time.Hour)
	goodCert := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			Organization: []string{"Acme Co"},
		},
		NotBefore:             goodNotBefore,
		NotAfter:              goodNotAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	expiredNotAfter := time.Now().Add(-time.Hour)
	expiredCert := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			Organization: []string{"Acme Co"},
		},
		NotBefore:             goodNotBefore,
		NotAfter:              expiredNotAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	missingNotBeforeCert := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			Organization: []string{"Acme Co"},
		},
		NotAfter:              goodNotAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	missingNotAfterCert := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			Organization: []string{"Acme Co"},
		},
		NotBefore:             goodNotBefore,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	// notBefore is intentionally not a pointer type because go certificates don't have nullable time types
	// we cheat a bit and make not after nullable because there's no valid reason to create a cert with go's zero
	// time.
	// see the AddCertMetadata function for more info on this.
	type expected struct {
		notBefore time.Time
		notAfter  *time.Time
	}
	tests := []struct {
		name     string
		certs    []*x509.Certificate
		expected expected
	}{
		{
			"Valid cert",
			[]*x509.Certificate{&goodCert},
			expected{
				notBefore: goodNotBefore,
				notAfter:  &goodNotAfter,
			},
		},
		{
			"Expired cert",
			[]*x509.Certificate{&expiredCert},
			expected{
				notBefore: goodNotBefore,
				notAfter:  &expiredNotAfter,
			},
		},
		{
			"Missing not before",
			[]*x509.Certificate{&missingNotBeforeCert},
			expected{
				notAfter: &goodNotAfter,
			},
		},
		{
			"Missing not after",
			[]*x509.Certificate{&missingNotAfterCert},
			expected{
				notBefore: goodNotBefore,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			notBefore, notAfter := calculateCertTimestamps(tt.certs)

			require.Equal(t, tt.expected.notBefore, notBefore)
			if tt.expected.notAfter != nil {
				require.Equal(t, tt.expected.notAfter, notAfter)
			} else {
				require.Nil(t, notAfter)
			}
		})
	}
}

func parseCert(t *testing.T, pemStr string) *x509.Certificate {
	block, _ := pem.Decode([]byte(elasticCert))
	if block == nil {
		require.Fail(t, "Test cert could not be parsed")
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	require.NoError(t, err)
	return cert
}

var elasticCert = `-----BEGIN CERTIFICATE-----

RG7L
-----END CERTIFICATE-----`

var elasticChainCert = `-----BEGIN CERTIFICATE-----
MIIEizCCA3OgAwIBAgIORvCM288sVGbvMwHdXzQwDQYJKoZIhvcNAQELBQAwVzEL
MAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsT
B1Jvb3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw0xNTA4MTkw
MDAwMDBaFw0yNTA4MTkwMDAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBH
bG9iYWxTaWduIG52LXNhMS0wKwYDVQQDEyRHbG9iYWxTaWduIENsb3VkU1NMIENB
IC0gU0hBMjU2IC0gRzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCj
wHXhMpjl2a6EfI3oI19GlVtMoiVw15AEhYDJtfSKZU2Sy6XEQqC2eSUx7fGFIM0T
UT1nrJdNaJszhlyzey2q33egYdH1PPua/NPVlMrJHoAbkJDIrI32YBecMbjFYaLi
blclCG8kmZnPlL/Hi2uwH8oU+hibbBB8mSvaSmPlsk7C/T4QC0j0dwsv8JZLOu69
Nd6FjdoTDs4BxHHT03fFCKZgOSWnJ2lcg9FvdnjuxURbRb0pO+LGCQ+ivivc41za
Wm+O58kHa36hwFOVgongeFxyqGy+Z2ur5zPZh/L4XCf09io7h+/awkfav6zrJ2R7
TFPrNOEvmyBNVBJrfSi9AgMBAAGjggFTMIIBTzAOBgNVHQ8BAf8EBAMCAQYwHQYD
VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAw
HQYDVR0OBBYEFKkrh+HOJEc7G7/PhTcCVZ0NlFjmMB8GA1UdIwQYMBaAFGB7ZhpF
DZfKiVAvfQTNNKj//P1LMD0GCCsGAQUFBwEBBDEwLzAtBggrBgEFBQcwAYYhaHR0
cDovL29jc3AuZ2xvYmFsc2lnbi5jb20vcm9vdHIxMDMGA1UdHwQsMCowKKAmoCSG
Imh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vcm9vdC5jcmwwVgYDVR0gBE8wTTAL
BgkrBgEEAaAyARQwPgYGZ4EMAQICMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3
Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQCi
HWmKCo7EFIMqKhJNOSeQTvCNrNKWYkc2XpLR+sWTtTcHZSnS9FNQa8n0/jT13bgd
+vzcFKxWlCecQqoETbftWNmZ0knmIC/Tp3e4Koka76fPhi3WU+kLk5xOq9lF7qSE
hf805A7Au6XOX5WJhXCqwV3szyvT2YPfA8qBpwIyt3dhECVO2XTz2XmCtSZwtFK8
jzPXiq4Z0PySrS+6PKBIWEde/SBWlSDBch2rZpmk1Xg3SBufskw3Z3r9QtLTVp7T
HY7EDGiWtkdREPd76xUJZPX58GMWLT3fI0I6k2PMq69PVwbH/hRVYs4nERnh9ELt
IjBrNRpKBYCkZd/My2/Q
-----END CERTIFICATE-----`
