package pki

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"math/big"
	mathrand "math/rand"
	"strings"
	"testing"
	"time"

	"github.com/go-test/deep"
	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/helper/certutil"
	vaulthttp "github.com/hashicorp/vault/http"
	"github.com/hashicorp/vault/logical"
	"github.com/hashicorp/vault/vault"
)

func TestBackend_CA_Steps(t *testing.T) {
	var b *backend

	factory := func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
		be, err := Factory(ctx, conf)
		if err == nil {
			b = be.(*backend)
		}
		return be, err
	}

	coreConfig := &vault.CoreConfig{
		LogicalBackends: map[string]logical.Factory{
			"pki": factory,
		},
	}
	cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()

	client := cluster.Cores[0].Client

	// Set RSA/EC CA certificates
	var rsaCAKey, rsaCACert, ecCAKey, ecCACert string
	{
		cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		if err != nil {
			panic(err)
		}
		marshaledKey, err := x509.MarshalECPrivateKey(cak)
		if err != nil {
			panic(err)
		}
		keyPEMBlock := &pem.Block{
			Type:  "EC PRIVATE KEY",
			Bytes: marshaledKey,
		}
		ecCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock)))
		if err != nil {
			panic(err)
		}
		subjKeyID, err := certutil.GetSubjKeyID(cak)
		if err != nil {
			panic(err)
		}
		caCertTemplate := &x509.Certificate{
			Subject: pkix.Name{
				CommonName: "root.localhost",
			},
			SubjectKeyId:          subjKeyID,
			DNSNames:              []string{"root.localhost"},
			KeyUsage:              x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign),
			SerialNumber:          big.NewInt(mathrand.Int63()),
			NotBefore:             time.Now().Add(-30 * time.Second),
			NotAfter:              time.Now().Add(262980 * time.Hour),
			BasicConstraintsValid: true,
			IsCA: true,
		}
		caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, cak.Public(), cak)
		if err != nil {
			panic(err)
		}
		caCertPEMBlock := &pem.Block{
			Type:  "CERTIFICATE",
			Bytes: caBytes,
		}
		ecCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock)))

		rak, err := rsa.GenerateKey(rand.Reader, 2048)
		if err != nil {
			panic(err)
		}
		marshaledKey = x509.MarshalPKCS1PrivateKey(rak)
		keyPEMBlock = &pem.Block{
			Type:  "RSA PRIVATE KEY",
			Bytes: marshaledKey,
		}
		rsaCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock)))
		if err != nil {
			panic(err)
		}
		subjKeyID, err = certutil.GetSubjKeyID(rak)
		if err != nil {
			panic(err)
		}
		caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, rak.Public(), rak)
		if err != nil {
			panic(err)
		}
		caCertPEMBlock = &pem.Block{
			Type:  "CERTIFICATE",
			Bytes: caBytes,
		}
		rsaCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock)))
	}

	// Setup backends
	var rsaRoot, rsaInt, ecRoot, ecInt *backend
	{
		if err := client.Sys().Mount("rsaroot", &api.MountInput{
			Type: "pki",
			Config: api.MountConfigInput{
				DefaultLeaseTTL: "16h",
				MaxLeaseTTL:     "60h",
			},
		}); err != nil {
			t.Fatal(err)
		}
		rsaRoot = b

		if err := client.Sys().Mount("rsaint", &api.MountInput{
			Type: "pki",
			Config: api.MountConfigInput{
				DefaultLeaseTTL: "16h",
				MaxLeaseTTL:     "60h",
			},
		}); err != nil {
			t.Fatal(err)
		}
		rsaInt = b

		if err := client.Sys().Mount("ecroot", &api.MountInput{
			Type: "pki",
			Config: api.MountConfigInput{
				DefaultLeaseTTL: "16h",
				MaxLeaseTTL:     "60h",
			},
		}); err != nil {
			t.Fatal(err)
		}
		ecRoot = b

		if err := client.Sys().Mount("ecint", &api.MountInput{
			Type: "pki",
			Config: api.MountConfigInput{
				DefaultLeaseTTL: "16h",
				MaxLeaseTTL:     "60h",
			},
		}); err != nil {
			t.Fatal(err)
		}
		ecInt = b
	}

	t.Run("teststeps", func(t *testing.T) {
		t.Run("rsa", func(t *testing.T) {
			t.Parallel()
			subClient, err := client.Clone()
			if err != nil {
				t.Fatal(err)
			}
			subClient.SetToken(client.Token())
			runSteps(t, rsaRoot, rsaInt, subClient, "rsaroot/", "rsaint/", rsaCACert, rsaCAKey)
		})
		t.Run("ec", func(t *testing.T) {
			t.Parallel()
			subClient, err := client.Clone()
			if err != nil {
				t.Fatal(err)
			}
			subClient.SetToken(client.Token())
			runSteps(t, ecRoot, ecInt, subClient, "ecroot/", "ecint/", ecCACert, ecCAKey)
		})
	})
}

func runSteps(t *testing.T, rootB, intB *backend, client *api.Client, rootName, intName, caCert, caKey string) {
	//  Load CA cert/key in and ensure we can fetch it back in various formats,
	//  unauthenticated
	{
		// Attempt import but only provide one the cert
		{
			_, err := client.Logical().Write(rootName+"config/ca", map[string]interface{}{
				"pem_bundle": caCert,
			})
			if err == nil {
				t.Fatal("expected error")
			}
		}

		// Same but with only the key
		{
			_, err := client.Logical().Write(rootName+"config/ca", map[string]interface{}{
				"pem_bundle": caKey,
			})
			if err == nil {
				t.Fatal("expected error")
			}
		}

		// Import CA bundle
		{
			_, err := client.Logical().Write(rootName+"config/ca", map[string]interface{}{
				"pem_bundle": strings.Join([]string{caKey, caCert}, "\n"),
			})
			if err != nil {
				t.Fatal(err)
			}
		}

		prevToken := client.Token()
		client.SetToken("")

		// cert/ca path
		{
			resp, err := client.Logical().Read(rootName + "cert/ca")
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			if diff := deep.Equal(resp.Data["certificate"].(string), caCert); diff != nil {
				t.Fatal(diff)
			}
		}
		// ca/pem path (raw string)
		{
			req := &logical.Request{
				Path:      "ca/pem",
				Operation: logical.ReadOperation,
				Storage:   rootB.storage,
			}
			resp, err := rootB.HandleRequest(context.Background(), req)
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			if diff := deep.Equal(resp.Data["http_raw_body"].([]byte), []byte(caCert)); diff != nil {
				t.Fatal(diff)
			}
			if resp.Data["http_content_type"].(string) != "application/pkix-cert" {
				t.Fatal("wrong content type")
			}
		}

		// ca (raw DER bytes)
		{
			req := &logical.Request{
				Path:      "ca",
				Operation: logical.ReadOperation,
				Storage:   rootB.storage,
			}
			resp, err := rootB.HandleRequest(context.Background(), req)
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			rawBytes := resp.Data["http_raw_body"].([]byte)
			pemBytes := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{
				Type:  "CERTIFICATE",
				Bytes: rawBytes,
			})))
			if diff := deep.Equal(pemBytes, caCert); diff != nil {
				t.Fatal(diff)
			}
			if resp.Data["http_content_type"].(string) != "application/pkix-cert" {
				t.Fatal("wrong content type")
			}
		}

		client.SetToken(prevToken)
	}

	// Configure an expiry on the CRL and verify what comes back
	{
		// Set CRL config
		{
			_, err := client.Logical().Write(rootName+"config/crl", map[string]interface{}{
				"expiry": "16h",
			})
			if err != nil {
				t.Fatal(err)
			}
		}

		// Verify it
		{
			resp, err := client.Logical().Read(rootName + "config/crl")
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			if resp.Data["expiry"].(string) != "16h" {
				t.Fatal("expected a 16 hour expiry")
			}
		}
	}

	// Test generating a root, an intermediate, signing it, setting signed, and
	// revoking it

	// We'll need this later
	var intSerialNumber string
	{
		// First, delete the existing CA info
		{
			_, err := client.Logical().Delete(rootName + "root")
			if err != nil {
				t.Fatal(err)
			}
		}

		var rootPEM, rootKey, rootPEMBundle string
		// Test exported root generation
		{
			resp, err := client.Logical().Write(rootName+"root/generate/exported", map[string]interface{}{
				"common_name": "Root Cert",
				"ttl":         "180h",
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			rootPEM = resp.Data["certificate"].(string)
			rootKey = resp.Data["private_key"].(string)
			rootPEMBundle = strings.Join([]string{rootPEM, rootKey}, "\n")
			// This is really here to keep the use checker happy
			if rootPEMBundle == "" {
				t.Fatal("bad root pem bundle")
			}
		}

		var intPEM, intCSR, intKey string
		// Test exported intermediate CSR generation
		{
			resp, err := client.Logical().Write(intName+"intermediate/generate/exported", map[string]interface{}{
				"common_name": "intermediate.cert.com",
				"ttl":         "180h",
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			intCSR = resp.Data["csr"].(string)
			intKey = resp.Data["private_key"].(string)
			// This is really here to keep the use checker happy
			if intCSR == "" || intKey == "" {
				t.Fatal("int csr or key empty")
			}
		}

		// Test signing
		{
			resp, err := client.Logical().Write(rootName+"root/sign-intermediate", map[string]interface{}{
				"common_name": "intermediate.cert.com",
				"ttl":         "10s",
				"csr":         intCSR,
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			intPEM = resp.Data["certificate"].(string)
			intSerialNumber = resp.Data["serial_number"].(string)
		}

		// Test setting signed
		{
			resp, err := client.Logical().Write(intName+"intermediate/set-signed", map[string]interface{}{
				"certificate": intPEM,
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp != nil {
				t.Fatal("expected nil response")
			}
		}

		// Verify we can find it via the root
		{
			resp, err := client.Logical().Read(rootName + "cert/" + intSerialNumber)
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			if resp.Data["revocation_time"].(json.Number).String() != "0" {
				t.Fatal("expected a zero revocation time")
			}
		}

		// Revoke the intermediate
		{
			resp, err := client.Logical().Write(rootName+"revoke", map[string]interface{}{
				"serial_number": intSerialNumber,
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
		}
	}

	verifyRevocation := func(t *testing.T, serial string, shouldFind bool) {
		// Verify it is now revoked
		{
			resp, err := client.Logical().Read(rootName + "cert/" + intSerialNumber)
			if err != nil {
				t.Fatal(err)
			}
			switch shouldFind {
			case true:
				if resp == nil {
					t.Fatal("nil response")
				}
				if resp.Data["revocation_time"].(json.Number).String() == "0" {
					t.Fatal("expected a non-zero revocation time")
				}
			default:
				if resp != nil {
					t.Fatalf("expected nil response, got %#v", *resp)
				}
			}
		}

		// Fetch the CRL and make sure it shows up
		{
			req := &logical.Request{
				Path:      "crl",
				Operation: logical.ReadOperation,
				Storage:   rootB.storage,
			}
			resp, err := rootB.HandleRequest(context.Background(), req)
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("nil response")
			}
			crlBytes := resp.Data["http_raw_body"].([]byte)
			certList, err := x509.ParseCRL(crlBytes)
			if err != nil {
				t.Fatal(err)
			}
			switch shouldFind {
			case true:
				revokedList := certList.TBSCertList.RevokedCertificates
				if len(revokedList) != 1 {
					t.Fatalf("bad length of revoked list: %d", len(revokedList))
				}
				revokedString := certutil.GetHexFormatted(revokedList[0].SerialNumber.Bytes(), ":")
				if revokedString != intSerialNumber {
					t.Fatalf("bad revoked serial: %s", revokedString)
				}
			default:
				revokedList := certList.TBSCertList.RevokedCertificates
				if len(revokedList) != 0 {
					t.Fatalf("bad length of revoked list: %d", len(revokedList))
				}
			}
		}
	}

	// Validate current state of revoked certificates
	verifyRevocation(t, intSerialNumber, true)

	// Give time for the safety buffer to pass before tidying
	time.Sleep(10 * time.Second)

	// Test tidying
	{
		// Run with a high safety buffer, nothing should happen
		{
			resp, err := client.Logical().Write(rootName+"tidy", map[string]interface{}{
				"safety_buffer":        "3h",
				"tidy_cert_store":      true,
				"tidy_revocation_list": true,
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("expected warnings")
			}

			// Wait a few seconds as it runs in a goroutine
			time.Sleep(5 * time.Second)

			// Check to make sure we still find the cert and see it on the CRL
			verifyRevocation(t, intSerialNumber, true)
		}

		// Run with both values set false, nothing should happen
		{
			resp, err := client.Logical().Write(rootName+"tidy", map[string]interface{}{
				"safety_buffer":        "1s",
				"tidy_cert_store":      false,
				"tidy_revocation_list": false,
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("expected warnings")
			}

			// Wait a few seconds as it runs in a goroutine
			time.Sleep(5 * time.Second)

			// Check to make sure we still find the cert and see it on the CRL
			verifyRevocation(t, intSerialNumber, true)
		}

		// Run with a short safety buffer and both set to true, both should be cleared
		{
			resp, err := client.Logical().Write(rootName+"tidy", map[string]interface{}{
				"safety_buffer":        "1s",
				"tidy_cert_store":      true,
				"tidy_revocation_list": true,
			})
			if err != nil {
				t.Fatal(err)
			}
			if resp == nil {
				t.Fatal("expected warnings")
			}

			// Wait a few seconds as it runs in a goroutine
			time.Sleep(5 * time.Second)

			// Check to make sure we still find the cert and see it on the CRL
			verifyRevocation(t, intSerialNumber, false)
		}
	}
}
