// Copyright 2016 Google LLC. All Rights Reserved.
//
// 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 ctfe

import (
	"crypto"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/go-sql-driver/mysql"
	ct "github.com/google/certificate-transparency-go"
	"github.com/google/certificate-transparency-go/trillian/ctfe/configpb"
	"github.com/google/certificate-transparency-go/x509"
	"github.com/jackc/pgx/v5/pgconn"
	"google.golang.org/protobuf/encoding/prototext"
	"google.golang.org/protobuf/proto"
	"k8s.io/klog/v2"
)

// ValidatedLogConfig represents the LogConfig with the information that has
// been successfully parsed as a result of validating it.
type ValidatedLogConfig struct {
	Config                               *configpb.LogConfig
	PubKey                               crypto.PublicKey
	PrivKey                              proto.Message
	KeyUsages                            []x509.ExtKeyUsage
	NotAfterStart                        *time.Time
	NotAfterLimit                        *time.Time
	FrozenSTH                            *ct.SignedTreeHead
	CTFEStorageConnectionString          string
	ExtraDataIssuanceChainStorageBackend configpb.LogConfig_IssuanceChainStorageBackend
}

// LogConfigFromFile creates a slice of LogConfig options from the given
// filename, which should contain text or binary-encoded protobuf configuration
// data.
func LogConfigFromFile(filename string) ([]*configpb.LogConfig, error) {
	cfgBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	var cfg configpb.LogConfigSet
	if txtErr := prototext.Unmarshal(cfgBytes, &cfg); txtErr != nil {
		if binErr := proto.Unmarshal(cfgBytes, &cfg); binErr != nil {
			return nil, fmt.Errorf("failed to parse LogConfigSet from %q as text protobuf (%v) or binary protobuf (%v)", filename, txtErr, binErr)
		}
	}

	if len(cfg.Config) == 0 {
		return nil, errors.New("empty log config found")
	}
	return cfg.Config, nil
}

// ToMultiLogConfig creates a multi backend config proto from the data
// loaded from a single-backend configuration file. All the log configs
// reference a default backend spec as provided.
func ToMultiLogConfig(cfg []*configpb.LogConfig, beSpec string) *configpb.LogMultiConfig {
	defaultBackend := &configpb.LogBackend{Name: "default", BackendSpec: beSpec}
	for _, c := range cfg {
		c.LogBackendName = defaultBackend.Name
	}
	return &configpb.LogMultiConfig{
		LogConfigs: &configpb.LogConfigSet{Config: cfg},
		Backends:   &configpb.LogBackendSet{Backend: []*configpb.LogBackend{defaultBackend}},
	}
}

// MultiLogConfigFromFile creates a LogMultiConfig proto from the given
// filename, which should contain text or binary-encoded protobuf configuration data.
// Does not do full validation of the config but checks that it is non empty.
func MultiLogConfigFromFile(filename string) (*configpb.LogMultiConfig, error) {
	cfgBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	var cfg configpb.LogMultiConfig
	if txtErr := prototext.Unmarshal(cfgBytes, &cfg); txtErr != nil {
		if binErr := proto.Unmarshal(cfgBytes, &cfg); binErr != nil {
			return nil, fmt.Errorf("failed to parse LogMultiConfig from %q as text protobuf (%v) or binary protobuf (%v)", filename, txtErr, binErr)
		}
	}

	if len(cfg.LogConfigs.GetConfig()) == 0 || len(cfg.Backends.GetBackend()) == 0 {
		return nil, errors.New("config is missing backends and/or log configs")
	}
	return &cfg, nil
}

// ValidateLogConfig checks that a single log config is valid. In particular:
//   - A mirror log has a valid public key and no private key.
//   - A non-mirror log has a private, and optionally a public key (both valid).
//   - Each of NotBeforeStart and NotBeforeLimit, if set, is a valid timestamp
//     proto. If both are set then NotBeforeStart <= NotBeforeLimit.
//   - Merge delays (if present) are correct.
//   - Frozen STH (if present) is correct and signed by the provided public key.
//
// Returns the validated structures (useful to avoid double validation).
func ValidateLogConfig(cfg *configpb.LogConfig) (*ValidatedLogConfig, error) {
	if cfg.LogId == 0 {
		return nil, errors.New("empty log ID")
	}

	vCfg := ValidatedLogConfig{Config: cfg}

	// Validate the public key.
	if pubKey := cfg.PublicKey; pubKey != nil {
		var err error
		if vCfg.PubKey, err = x509.ParsePKIXPublicKey(pubKey.Der); err != nil {
			return nil, fmt.Errorf("x509.ParsePKIXPublicKey: %w", err)
		}
	} else if cfg.IsMirror {
		return nil, errors.New("empty public key for mirror")
	} else if cfg.FrozenSth != nil {
		return nil, errors.New("empty public key for frozen STH")
	}

	// Validate the private key.
	if !cfg.IsMirror {
		if cfg.PrivateKey == nil {
			return nil, errors.New("empty private key")
		}
		privKey, err := cfg.PrivateKey.UnmarshalNew()
		if err != nil {
			return nil, fmt.Errorf("invalid private key: %v", err)
		}
		vCfg.PrivKey = privKey
	} else if cfg.PrivateKey != nil {
		return nil, errors.New("unnecessary private key for mirror")
	}

	if cfg.RejectExpired && cfg.RejectUnexpired {
		return nil, errors.New("rejecting all certificates")
	}

	// Validate the extended key usages list.
	if len(cfg.ExtKeyUsages) > 0 {
		for _, kuStr := range cfg.ExtKeyUsages {
			if ku, ok := stringToKeyUsage[kuStr]; ok {
				// If "Any" is specified, then we can ignore the entire list and
				// just disable EKU checking.
				if ku == x509.ExtKeyUsageAny {
					klog.Infof("%s: Found ExtKeyUsageAny, allowing all EKUs", cfg.Prefix)
					vCfg.KeyUsages = nil
					break
				}
				vCfg.KeyUsages = append(vCfg.KeyUsages, ku)
			} else {
				return nil, fmt.Errorf("unknown extended key usage: %s", kuStr)
			}
		}
	}

	// Validate the time interval.
	start, limit := cfg.NotAfterStart, cfg.NotAfterLimit
	if start != nil {
		vCfg.NotAfterStart = &time.Time{}
		if err := start.CheckValid(); err != nil {
			return nil, fmt.Errorf("invalid start timestamp: %v", err)
		}
		*vCfg.NotAfterStart = start.AsTime()
	}
	if limit != nil {
		vCfg.NotAfterLimit = &time.Time{}
		if err := limit.CheckValid(); err != nil {
			return nil, fmt.Errorf("invalid limit timestamp: %v", err)
		}
		*vCfg.NotAfterLimit = limit.AsTime()
	}
	if start != nil && limit != nil && (*vCfg.NotAfterLimit).Before(*vCfg.NotAfterStart) {
		return nil, errors.New("limit before start")
	}

	switch {
	case cfg.MaxMergeDelaySec < 0:
		return nil, errors.New("negative maximum merge delay")
	case cfg.ExpectedMergeDelaySec < 0:
		return nil, errors.New("negative expected merge delay")
	case cfg.ExpectedMergeDelaySec > cfg.MaxMergeDelaySec:
		return nil, errors.New("expected merge delay exceeds MMD")
	}

	if sth := cfg.FrozenSth; sth != nil {
		verifier, err := ct.NewSignatureVerifier(vCfg.PubKey)
		if err != nil {
			return nil, fmt.Errorf("failed to create signature verifier: %v", err)
		}
		if vCfg.FrozenSTH, err = (&ct.GetSTHResponse{
			TreeSize:          uint64(sth.TreeSize),
			Timestamp:         uint64(sth.Timestamp),
			SHA256RootHash:    sth.Sha256RootHash,
			TreeHeadSignature: sth.TreeHeadSignature,
		}).ToSignedTreeHead(); err != nil {
			return nil, fmt.Errorf("invalid frozen STH: %v", err)
		}
		if err := verifier.VerifySTHSignature(*vCfg.FrozenSTH); err != nil {
			return nil, fmt.Errorf("signature verification failed: %v", err)
		}
	}

	switch cfg.ExtraDataIssuanceChainStorageBackend {
	case configpb.LogConfig_ISSUANCE_CHAIN_STORAGE_BACKEND_CTFE:
		// Validate the combination of CtfeStorageConnectionString and ExtraDataIssuanceChainStorageBackend
		if len(cfg.CtfeStorageConnectionString) == 0 {
			return nil, errors.New("missing ctfe_storage_connection_string when issuance chain storage backend is CTFE")
		}
		// Validate CTFEStorageConnectionString
		if strings.HasPrefix(cfg.CtfeStorageConnectionString, "mysql") {
			if _, err := mysql.ParseDSN(strings.Split(cfg.CtfeStorageConnectionString, "://")[1]); err != nil {
				return nil, errors.New("failed to parse ctfe_storage_connection_string for mysql driver")
			}
		} else if strings.HasPrefix(cfg.CtfeStorageConnectionString, "postgres") {
			if _, err := pgconn.ParseConfig(cfg.CtfeStorageConnectionString); err != nil {
				return nil, errors.New("failed to parse ctfe_storage_connection_string for postgresql pgx driver")
			}
		} else {
			return nil, errors.New("unsupported driver in ctfe_storage_connection_string")
		}
		vCfg.CTFEStorageConnectionString = cfg.CtfeStorageConnectionString
	case configpb.LogConfig_ISSUANCE_CHAIN_STORAGE_BACKEND_TRILLIAN_GRPC:
		// Nothing to validate for Trillian gRPC
	}
	vCfg.ExtraDataIssuanceChainStorageBackend = cfg.ExtraDataIssuanceChainStorageBackend

	return &vCfg, nil
}

// LogBackendMap is a map from log backend names to LogBackend objects.
type LogBackendMap = map[string]*configpb.LogBackend

// BuildLogBackendMap returns a map from log backend names to the corresponding
// LogBackend objects. It returns an error unless all backends have unique
// non-empty names and specifications.
func BuildLogBackendMap(lbs *configpb.LogBackendSet) (LogBackendMap, error) {
	lbm := make(LogBackendMap)
	specs := make(map[string]bool)
	for _, be := range lbs.Backend {
		if len(be.Name) == 0 {
			return nil, fmt.Errorf("empty backend name: %v", be)
		}
		if len(be.BackendSpec) == 0 {
			return nil, fmt.Errorf("empty backend spec: %v", be)
		}
		if _, ok := lbm[be.Name]; ok {
			return nil, fmt.Errorf("duplicate backend name: %v", be)
		}
		if ok := specs[be.BackendSpec]; ok {
			return nil, fmt.Errorf("duplicate backend spec: %v", be)
		}
		lbm[be.Name] = be
		specs[be.BackendSpec] = true
	}
	return lbm, nil
}

func validateConfigs(cfg []*configpb.LogConfig) error {
	// Check that logs have no duplicate or empty prefixes. Apply other LogConfig
	// specific checks.
	logNameMap := make(map[string]bool)
	for _, logCfg := range cfg {
		if _, err := ValidateLogConfig(logCfg); err != nil {
			return fmt.Errorf("log config: %v: %v", err, logCfg)
		}
		if len(logCfg.Prefix) == 0 {
			return fmt.Errorf("log config: empty prefix: %v", logCfg)
		}
		if logNameMap[logCfg.Prefix] {
			return fmt.Errorf("log config: duplicate prefix: %s: %v", logCfg.Prefix, logCfg)
		}
		logNameMap[logCfg.Prefix] = true
	}

	return nil
}

// ValidateLogConfigs checks that a config is valid for use with a single log
// server. The rules applied are:
//
// 1. All log configs must be valid (see ValidateLogConfig).
// 2. The prefixes of configured logs must all be distinct and must not be
// empty.
// 3. The set of tree IDs must be distinct.
func ValidateLogConfigs(cfg []*configpb.LogConfig) error {
	if err := validateConfigs(cfg); err != nil {
		return err
	}

	// Check that logs have no duplicate tree IDs.
	treeIDs := make(map[int64]bool)
	for _, logCfg := range cfg {
		if treeIDs[logCfg.LogId] {
			return fmt.Errorf("log config: dup tree id: %d for: %v", logCfg.LogId, logCfg)
		}
		treeIDs[logCfg.LogId] = true
	}

	return nil
}

// ValidateLogMultiConfig checks that a config is valid for use with multiple
// backend log servers. The rules applied are the same as ValidateLogConfigs, as
// well as these additional rules:
//
// 1. The backend set must define a set of log backends with distinct
// (non empty) names and non empty backend specs.
// 2. The backend specs must all be distinct.
// 3. The log configs must all specify a log backend and each must be one of
// those defined in the backend set.
//
// Also, another difference is that the tree IDs need only to be distinct per
// backend.
//
// TODO(pavelkalinnikov): Replace the returned map with a fully fledged
// ValidatedLogMultiConfig that contains a ValidatedLogConfig for each log.
func ValidateLogMultiConfig(cfg *configpb.LogMultiConfig) (LogBackendMap, error) {
	backendMap, err := BuildLogBackendMap(cfg.Backends)
	if err != nil {
		return nil, err
	}

	if err := validateConfigs(cfg.GetLogConfigs().GetConfig()); err != nil {
		return nil, err
	}

	// Check that logs all reference a defined backend.
	logIDMap := make(map[string]bool)
	for _, logCfg := range cfg.LogConfigs.Config {
		if _, ok := backendMap[logCfg.LogBackendName]; !ok {
			return nil, fmt.Errorf("log config: references undefined backend: %s: %v", logCfg.LogBackendName, logCfg)
		}
		logIDKey := fmt.Sprintf("%s-%d", logCfg.LogBackendName, logCfg.LogId)
		if ok := logIDMap[logIDKey]; ok {
			return nil, fmt.Errorf("log config: dup tree id: %d for: %v", logCfg.LogId, logCfg)
		}
		logIDMap[logIDKey] = true
	}

	return backendMap, nil
}

var stringToKeyUsage = map[string]x509.ExtKeyUsage{
	"Any":                        x509.ExtKeyUsageAny,
	"ServerAuth":                 x509.ExtKeyUsageServerAuth,
	"ClientAuth":                 x509.ExtKeyUsageClientAuth,
	"CodeSigning":                x509.ExtKeyUsageCodeSigning,
	"EmailProtection":            x509.ExtKeyUsageEmailProtection,
	"IPSECEndSystem":             x509.ExtKeyUsageIPSECEndSystem,
	"IPSECTunnel":                x509.ExtKeyUsageIPSECTunnel,
	"IPSECUser":                  x509.ExtKeyUsageIPSECUser,
	"TimeStamping":               x509.ExtKeyUsageTimeStamping,
	"OCSPSigning":                x509.ExtKeyUsageOCSPSigning,
	"MicrosoftServerGatedCrypto": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
	"NetscapeServerGatedCrypto":  x509.ExtKeyUsageNetscapeServerGatedCrypto,
}
