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

package util

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

	"github.com/hashicorp/go-multierror"
)

// ExtractCertificates takes the contents of the file at CF_INSTANCE_CERT, which typically are
// comprised of two certificates. One is the identity certificate, and one is an intermediate
// CA certificate which is crucial in linking the identity cert back to the configured root
// certificate. It splits these two certificates apart, and identifies the certificate marked
// as a CA as the intermediate cert, and the one not marked as a CA as the identity certificate.
// It may error if the given file contents or certificates aren't as expected.
func ExtractCertificates(cfInstanceCertContents string) (intermediateCert, identityCert *x509.Certificate, err error) {
	certPairBytes := []byte(cfInstanceCertContents)
	numCerts := 0
	var block *pem.Block
	var result error
	for {
		block, certPairBytes = pem.Decode(certPairBytes)
		if block == nil {
			break
		}
		certs, err := x509.ParseCertificates(block.Bytes)
		if err != nil {
			result = multierror.Append(result, err)
			continue
		}
		for _, cert := range certs {
			if cert.IsCA {
				intermediateCert = cert
			} else {
				identityCert = cert
			}
			numCerts++
		}
	}
	if numCerts != 2 {
		result = multierror.Append(fmt.Errorf("expected 2 certs but received %s", certPairBytes))
	}
	if intermediateCert == nil {
		result = multierror.Append(fmt.Errorf("no intermediate certificate found in %s", certPairBytes))
	}
	if identityCert == nil {
		result = multierror.Append(fmt.Errorf("no identity cert found in %s", certPairBytes))
	}
	return intermediateCert, identityCert, result
}

// Validate takes a group of trusted CA certificates, an intermediate certificate, an identity certificate,
// and a signing certificate, and makes sure they have the following properties:
//   - The identity certificate is the same as the signing certificate
//   - The identity certificate chains to at least one trusted CA
func Validate(caCerts []string, intermediateCert, identityCert, signingCert *x509.Certificate) error {
	if !reflect.DeepEqual(identityCert, signingCert) {
		return errors.New("signature not generated by identity cert")
	}
	roots := x509.NewCertPool()
	for _, caCert := range caCerts {
		if ok := roots.AppendCertsFromPEM([]byte(caCert)); !ok {
			return errors.New("couldn't append root certificate")
		}
	}
	intermediates := x509.NewCertPool()
	intermediates.AddCert(intermediateCert)
	verifyOpts := x509.VerifyOptions{
		Roots:         roots,
		Intermediates: intermediates,
	}
	if _, err := signingCert.Verify(verifyOpts); err != nil {
		return err
	}
	return nil
}
