// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package structs

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"

	"github.com/miekg/dns"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/version"
)

// InlineCertificateConfigEntry manages the configuration for an inline certificate
// with the given name.
type InlineCertificateConfigEntry struct {
	// Kind of config entry. This will be set to structs.InlineCertificate.
	Kind string

	// Name is used to match the config entry with its associated inline certificate.
	Name string

	// Certificate is the public certificate component of an x509 key pair encoded in raw PEM format.
	Certificate string
	// PrivateKey is the private key component of an x509 key pair encoded in raw PEM format.
	PrivateKey string

	Meta               map[string]string `json:",omitempty"`
	Hash               uint64            `json:",omitempty" hash:"ignore"`
	acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
	RaftIndex          `hash:"ignore"`
}

func (e *InlineCertificateConfigEntry) SetHash(h uint64) {
	e.Hash = h
}

func (e *InlineCertificateConfigEntry) GetHash() uint64 {
	return e.Hash
}

func (e *InlineCertificateConfigEntry) GetKind() string { return InlineCertificate }
func (e *InlineCertificateConfigEntry) GetName() string { return e.Name }
func (e *InlineCertificateConfigEntry) Normalize() error {
	h, err := HashConfigEntry(e)
	if err != nil {
		return err
	}
	e.Hash = h
	return nil
}
func (e *InlineCertificateConfigEntry) GetMeta() map[string]string { return e.Meta }
func (e *InlineCertificateConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
	return &e.EnterpriseMeta
}
func (e *InlineCertificateConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex }

// Envoy will silently reject any RSA keys that are less than 2048 bytes long
// https://github.com/envoyproxy/envoy/blob/main/source/extensions/transport_sockets/tls/context_impl.cc#L238
const MinKeyLength = 2048

func (e *InlineCertificateConfigEntry) Validate() error {
	err := validateConfigEntryMeta(e.Meta)
	if err != nil {
		return err
	}

	privateKeyBlock, _ := pem.Decode([]byte(e.PrivateKey))
	if privateKeyBlock == nil {
		return errors.New("failed to parse private key PEM")
	}
	err = validateKeyLength(privateKeyBlock)
	if err != nil {
		return err
	}

	certificateBlock, _ := pem.Decode([]byte(e.Certificate))
	if certificateBlock == nil {
		return errors.New("failed to parse certificate PEM")
	}

	// make sure we have a valid x509 certificate
	_, err = x509.ParseCertificate(certificateBlock.Bytes)
	if err != nil {
		return fmt.Errorf("failed to parse certificate: %w", err)
	}

	// validate that the cert was generated with the given private key
	_, err = tls.X509KeyPair([]byte(e.Certificate), []byte(e.PrivateKey))
	if err != nil {
		return err
	}

	// validate that each host referenced in the CN, DNSSans, and IPSans
	// are valid hostnames
	hosts, err := e.Hosts()
	if err != nil {
		return err
	}
	for _, host := range hosts {
		if _, ok := dns.IsDomainName(host); !ok {
			return fmt.Errorf("host %q must be a valid DNS hostname", host)
		}
	}

	return nil
}

func validateKeyLength(privateKeyBlock *pem.Block) error {
	if privateKeyBlock.Type != "RSA PRIVATE KEY" {
		return nil
	}

	key, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
	if err != nil {
		return err
	}

	keyBitLen := key.N.BitLen()

	if version.IsFIPS() {
		return fipsLenCheck(keyBitLen)
	}

	return nonFipsLenCheck(keyBitLen)
}

func nonFipsLenCheck(keyLen int) error {
	// ensure private key is of the correct length
	if keyLen < MinKeyLength {
		return errors.New("key length must be at least 2048 bits")
	}

	return nil
}

func fipsLenCheck(keyLen int) error {
	if keyLen != 2048 && keyLen != 3072 && keyLen != 4096 {
		return errors.New("key length invalid: only RSA lengths of 2048, 3072, and 4096 are allowed in FIPS mode")
	}
	return nil
}

func (e *InlineCertificateConfigEntry) Hosts() ([]string, error) {
	certificateBlock, _ := pem.Decode([]byte(e.Certificate))
	if certificateBlock == nil {
		return nil, errors.New("failed to parse certificate PEM")
	}

	certificate, err := x509.ParseCertificate(certificateBlock.Bytes)
	if err != nil {
		return nil, fmt.Errorf("failed to parse certificate: %w", err)
	}

	hosts := []string{certificate.Subject.CommonName}

	for _, name := range certificate.DNSNames {
		hosts = append(hosts, name)
	}

	for _, ip := range certificate.IPAddresses {
		hosts = append(hosts, ip.String())
	}

	return hosts, nil
}

func (e *InlineCertificateConfigEntry) CanRead(authz acl.Authorizer) error {
	var authzContext acl.AuthorizerContext
	e.FillAuthzContext(&authzContext)
	return authz.ToAllowAuthorizer().MeshReadAllowed(&authzContext)
}

func (e *InlineCertificateConfigEntry) CanWrite(authz acl.Authorizer) error {
	var authzContext acl.AuthorizerContext
	e.FillAuthzContext(&authzContext)
	return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
}
