//  Copyright Istio 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 ca

import (
	"os"
	"path/filepath"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"istio.io/istio/pkg/test/cert"
	"istio.io/istio/pkg/test/util/file"
	"istio.io/istio/pkg/test/util/tmpl"
)

const (
	istioConfTemplate = `
[ req ]
encrypt_key = no
prompt = no
utf8 = yes
default_md = sha256
default_bits = 4096
req_extensions = req_ext
x509_extensions = req_ext
distinguished_name = req_dn
[ req_ext ]
subjectKeyIdentifier = hash
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, keyCertSign
subjectAltName=@san
[ san ]
DNS.1 = istiod.{{ .SystemNamespace }}
DNS.2 = istiod.{{ .SystemNamespace }}.svc
DNS.3 = istio-pilot.{{ .SystemNamespace }}
DNS.4 = istio-pilot.{{ .SystemNamespace }}.svc
[ req_dn ]
O = Istio
CN = Intermediate CA
`
)

// NewIstioConfig creates an extensions configuration for Istio, using the given system namespace in
// the DNS SANs.
func NewIstioConfig(systemNamespace string) (string, error) {
	return tmpl.Evaluate(istioConfTemplate, map[string]any{
		"SystemNamespace": systemNamespace,
	})
}

// IntermediateCA is an intermediate CA for a single cluster.
type Intermediate struct {
	KeyFile  string
	ConfFile string
	CSRFile  string
	CertFile string
	Root     Root
}

// NewIntermediate creates a new intermediate CA for the given cluster.
func NewIntermediate(workDir, config string, root Root) (Intermediate, error) {
	ca := Intermediate{
		KeyFile:  filepath.Join(workDir, "ca-key.pem"),
		ConfFile: filepath.Join(workDir, "ca.conf"),
		CSRFile:  filepath.Join(workDir, "ca.csr"),
		CertFile: filepath.Join(workDir, "ca-cert.pem"),
		Root:     root,
	}

	// Write out the CA config file.
	if err := os.WriteFile(ca.ConfFile, []byte(config), os.ModePerm); err != nil {
		return Intermediate{}, err
	}

	// Create the key for the intermediate CA.
	if err := cert.GenerateKey(ca.KeyFile); err != nil {
		return Intermediate{}, err
	}

	// Create the CSR for the intermediate CA.
	if err := cert.GenerateCSR(ca.ConfFile, ca.KeyFile, ca.CSRFile); err != nil {
		return Intermediate{}, err
	}

	// Create the intermediate cert, signed by the root.
	if err := cert.GenerateIntermediateCert(ca.ConfFile, ca.CSRFile, root.CertFile,
		root.KeyFile, ca.CertFile); err != nil {
		return Intermediate{}, err
	}

	return ca, nil
}

// NewIstioCASecret creates a secret (named "cacerts") containing the intermediate certificate and cert chain.
// If available when Istio starts, this will be used instead of Istio's autogenerated self-signed root
// (istio-ca-secret). This can be used in a multicluster environment in order to establish a common root of
// trust between the clusters.
func (ca Intermediate) NewIstioCASecret() (*corev1.Secret, error) {
	caCert, err := file.AsString(ca.CertFile)
	if err != nil {
		return nil, err
	}
	caKey, err := file.AsString(ca.KeyFile)
	if err != nil {
		return nil, err
	}
	rootCert, err := file.AsString(ca.Root.CertFile)
	if err != nil {
		return nil, err
	}

	// Create the cert chain by concatenating the intermediate and root certs.
	certChain := caCert + rootCert

	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cacerts",
		},
		Data: map[string][]byte{
			"ca-cert.pem":    []byte(caCert),
			"ca-key.pem":     []byte(caKey),
			"cert-chain.pem": []byte(certChain),
			"root-cert.pem":  []byte(rootCert),
		},
	}, nil
}
