package pki

import (
	"context"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/rsa"
	"crypto/subtle"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"strings"

	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/helper/certutil"
	"github.com/hashicorp/vault/sdk/helper/consts"
	"github.com/hashicorp/vault/sdk/helper/errutil"
	"github.com/hashicorp/vault/sdk/logical"
)

func pathRevoke(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: `revoke`,
		Fields: map[string]*framework.FieldSchema{
			"serial_number": {
				Type: framework.TypeString,
				Description: `Certificate serial number, in colon- or
hyphen-separated octal`,
			},
			"certificate": {
				Type: framework.TypeString,
				Description: `Certificate to revoke in PEM format; must be
signed by an issuer in this mount.`,
			},
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.metricsWrap("revoke", noRole, b.pathRevokeWrite),
				// This should never be forwarded. See backend.go for more information.
				// If this needs to write, the entire request will be forwarded to the
				// active node of the current performance cluster, but we don't want to
				// forward invalid revoke requests there.
			},
		},

		HelpSynopsis:    pathRevokeHelpSyn,
		HelpDescription: pathRevokeHelpDesc,
	}
}

func pathRevokeWithKey(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: `revoke-with-key`,
		Fields: map[string]*framework.FieldSchema{
			"serial_number": {
				Type: framework.TypeString,
				Description: `Certificate serial number, in colon- or
hyphen-separated octal`,
			},
			"certificate": {
				Type: framework.TypeString,
				Description: `Certificate to revoke in PEM format; must be
signed by an issuer in this mount.`,
			},
			"private_key": {
				Type: framework.TypeString,
				Description: `Key to use to verify revocation permission; must
be in PEM format.`,
			},
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.metricsWrap("revoke", noRole, b.pathRevokeWrite),
				// This should never be forwarded. See backend.go for more information.
				// If this needs to write, the entire request will be forwarded to the
				// active node of the current performance cluster, but we don't want to
				// forward invalid revoke requests there.
			},
		},

		HelpSynopsis:    pathRevokeHelpSyn,
		HelpDescription: pathRevokeHelpDesc,
	}
}

func pathRotateCRL(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: `crl/rotate`,

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				Callback: b.pathRotateCRLRead,
				// See backend.go; we will read a lot of data prior to calling write,
				// so this request should be forwarded when it is first seen, not
				// when it is ready to write.
				ForwardPerformanceStandby: true,
			},
		},

		HelpSynopsis:    pathRotateCRLHelpSyn,
		HelpDescription: pathRotateCRLHelpDesc,
	}
}

func pathRotateDeltaCRL(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: `crl/rotate-delta`,

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				Callback: b.pathRotateDeltaCRLRead,
				// See backend.go; we will read a lot of data prior to calling write,
				// so this request should be forwarded when it is first seen, not
				// when it is ready to write.
				ForwardPerformanceStandby: true,
			},
		},

		HelpSynopsis:    pathRotateDeltaCRLHelpSyn,
		HelpDescription: pathRotateDeltaCRLHelpDesc,
	}
}

func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *logical.Request, certPem string) (string, bool, []byte, error) {
	// This function handles just the verification of the certificate against
	// the global issuer set, checking whether or not it is importable.
	//
	// We return the parsed serial number, an optionally-nil byte array to
	// write out to disk, and an error if one occurred.
	if b.useLegacyBundleCaStorage() {
		// We require listing all issuers from the 1.11 method. If we're
		// still using the legacy CA bundle but with the newer certificate
		// attribute, we err and require the operator to upgrade and migrate
		// prior to servicing new requests.
		return "", false, nil, errutil.UserError{Err: "unable to process BYOC revocation until CA issuer migration has completed"}
	}

	// First start by parsing the certificate.
	if len(certPem) < 75 {
		// See note in pathImportIssuers about this check.
		return "", false, nil, errutil.UserError{Err: "provided certificate data was too short; perhaps a path was passed to the API rather than the contents of a PEM file"}
	}

	pemBlock, _ := pem.Decode([]byte(certPem))
	if pemBlock == nil {
		return "", false, nil, errutil.UserError{Err: "certificate contains no PEM data"}
	}

	certReference, err := x509.ParseCertificate(pemBlock.Bytes)
	if err != nil {
		return "", false, nil, errutil.UserError{Err: fmt.Sprintf("certificate could not be parsed: %v", err)}
	}

	// Ensure we have a well-formed serial number before continuing.
	serial := serialFromCert(certReference)
	if len(serial) == 0 {
		return "", false, nil, errutil.UserError{Err: "invalid serial number on presented certificate"}
	}

	// We have two approaches here: we could start verifying against issuers
	// (which involves fetching and parsing them), or we could see if, by
	// some chance we've already imported it (cheap). The latter tells us
	// if we happen to have a serial number collision (which shouldn't
	// happen in practice) versus an already-imported cert (which might
	// happen and its fine to handle safely).
	//
	// Start with the latter since its cheaper. Fetch the cert (by serial)
	// and if it exists, compare the contents.
	certEntry, err := fetchCertBySerial(ctx, b, req, req.Path, serial)
	if err != nil {
		return serial, false, nil, err
	}

	if certEntry != nil {
		// As seen with importing issuers, it is best to parse the certificate
		// and compare parsed values, rather than attempting to infer equality
		// from the raw data.
		certReferenceStored, err := x509.ParseCertificate(certEntry.Value)
		if err != nil {
			return serial, false, nil, err
		}

		if !areCertificatesEqual(certReference, certReferenceStored) {
			// Here we refuse the import with an error because the two certs
			// are unequal but we would've otherwise overwritten the existing
			// copy.
			return serial, false, nil, fmt.Errorf("certificate with same serial but unequal value already present in this cluster's storage; refusing to revoke")
		} else {
			// Otherwise, we can return without an error as we've already
			// imported this certificate, likely when we issued it. We don't
			// need to re-verify the signature as we assume it was already
			// verified when it was imported.
			return serial, false, certEntry.Value, nil
		}
	}

	// Otherwise, we must not have a stored copy. From here on out, the second
	// parameter (except in error cases) should cause the cert to write out.
	//
	// Fetch and iterate through each issuer.
	sc := b.makeStorageContext(ctx, req.Storage)
	issuers, err := sc.listIssuers()
	if err != nil {
		return serial, false, nil, err
	}

	foundMatchingIssuer := false
	for _, issuerId := range issuers {
		issuer, err := sc.fetchIssuerById(issuerId)
		if err != nil {
			return serial, false, nil, err
		}

		issuerCert, err := issuer.GetCertificate()
		if err != nil {
			return serial, false, nil, err
		}

		if err := certReference.CheckSignatureFrom(issuerCert); err == nil {
			// If the signature was valid, we found our match and can safely
			// exit.
			foundMatchingIssuer = true
			break
		}
	}

	if foundMatchingIssuer {
		return serial, true, certReference.Raw, nil
	}

	return serial, false, nil, errutil.UserError{Err: "unable to verify signature on presented cert from any present issuer in this mount; certificates from previous CAs will need to have their issuing CA and key re-imported if revocation is necessary"}
}

func (b *backend) pathRevokeWriteHandleKey(ctx context.Context, req *logical.Request, cert []byte, keyPem string) error {
	if keyPem == "" {
		// The only way to get here should be via the /revoke endpoint;
		// validate the path one more time and return an error if necessary.
		if req.Path != "revoke" {
			return fmt.Errorf("must have private key to revoke via the /revoke-with-key path")
		}

		// Otherwise, we don't need to validate the key and thus can return
		// with success.
		return nil
	}

	// Parse the certificate for reference.
	certReference, err := x509.ParseCertificate(cert)
	if err != nil {
		return errutil.UserError{Err: fmt.Sprintf("certificate could not be parsed: %v", err)}
	}

	// Now parse the key's PEM block.
	pemBlock, _ := pem.Decode([]byte(keyPem))
	if pemBlock == nil {
		return errutil.UserError{Err: "provided key PEM block contained no data or failed to parse"}
	}

	// Parse the inner DER key.
	signer, _, err := certutil.ParseDERKey(pemBlock.Bytes)
	if err != nil {
		return fmt.Errorf("failed to parse provided private key: %v", err)
	}

	// Finally, verify if the cert and key match. This code has been
	// cribbed from the Go TLS config code, with minor modifications.
	//
	// In particular, we validate against the derived public key
	// components and ensure we validate exponent and curve information
	// as well.
	//
	//
	// See: https://github.com/golang/go/blob/c6a2dada0df8c2d75cf3ae599d7caed77d416fa2/src/crypto/tls/tls.go#L304-L331
	switch certPub := certReference.PublicKey.(type) {
	case *rsa.PublicKey:
		privPub, ok := signer.Public().(*rsa.PublicKey)
		if !ok {
			return errutil.UserError{Err: "provided private key type does not match certificate's public key type"}
		}
		if err := signer.(*rsa.PrivateKey).Validate(); err != nil {
			return err
		}
		if certPub.N.Cmp(privPub.N) != 0 || certPub.E != privPub.E {
			return errutil.UserError{Err: "provided private key does not match certificate's public key"}
		}
	case *ecdsa.PublicKey:
		privPub, ok := signer.Public().(*ecdsa.PublicKey)
		if !ok {
			return errutil.UserError{Err: "provided private key type does not match certificate's public key type"}
		}
		if certPub.X.Cmp(privPub.X) != 0 || certPub.Y.Cmp(privPub.Y) != 0 || certPub.Params().Name != privPub.Params().Name {
			return errutil.UserError{Err: "provided private key does not match certificate's public key"}
		}
	case ed25519.PublicKey:
		privPub, ok := signer.Public().(ed25519.PublicKey)
		if !ok {
			return errutil.UserError{Err: "provided private key type does not match certificate's public key type"}
		}
		if subtle.ConstantTimeCompare(privPub, certPub) == 0 {
			return errutil.UserError{Err: "provided private key does not match certificate's public key"}
		}
	default:
		return errutil.UserError{Err: "certificate has an unknown public key algorithm; unable to validate provided private key; ask an admin to revoke this certificate instead"}
	}

	return nil
}

func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, data *framework.FieldData, _ *roleEntry) (*logical.Response, error) {
	rawSerial, haveSerial := data.GetOk("serial_number")
	rawCertificate, haveCert := data.GetOk("certificate")

	if !haveSerial && !haveCert {
		return logical.ErrorResponse("The serial number or certificate to revoke must be provided."), nil
	} else if haveSerial && haveCert {
		return logical.ErrorResponse("Must provide either the certificate or the serial to revoke; not both."), nil
	}

	var keyPem string
	if req.Path == "revoke-with-key" {
		rawKey, haveKey := data.GetOk("private_key")
		if !haveKey {
			return logical.ErrorResponse("Must have private key to revoke via the /revoke-with-key path."), nil
		}

		keyPem = rawKey.(string)
		if len(keyPem) < 64 {
			// See note in pathImportKeyHandler...
			return logical.ErrorResponse("Provided data for private_key was too short; perhaps a path was passed to the API rather than the contents of a PEM file?"), nil
		}
	}

	var serial string
	if haveSerial {
		// Easy case: this cert should be in storage already.
		serial = rawSerial.(string)
		if len(serial) == 0 {
			return logical.ErrorResponse("The serial number must be provided"), nil
		}

		// Here, fetch the certificate from disk to validate we can revoke it.
		certEntry, err := fetchCertBySerial(ctx, b, req, req.Path, serial)
		if err != nil {
			switch err.(type) {
			case errutil.UserError:
				return logical.ErrorResponse(err.Error()), nil
			default:
				return nil, err
			}
		}
		if certEntry == nil {
			return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
		}

		// Now, if the user provided a key, we'll have to make sure the key
		// and stored certificate match.
		if err := b.pathRevokeWriteHandleKey(ctx, req, certEntry.Value, keyPem); err != nil {
			return nil, err
		}
	} else {
		// Otherwise, we've gotta parse the certificate from the request and
		// then import it into cluster-local storage. Before writing the
		// certificate (and forwarding), we want to verify this certificate
		// was actually signed by one of our present issuers.
		var err error
		var writeCert bool
		var certBytes []byte
		serial, writeCert, certBytes, err = b.pathRevokeWriteHandleCertificate(ctx, req, rawCertificate.(string))
		if err != nil {
			return nil, err
		}

		// Before we write the certificate, we've gotta verify the request in
		// the event of a PoP-based revocation scheme; we don't want to litter
		// storage with issued-but-not-revoked certificates.
		if err := b.pathRevokeWriteHandleKey(ctx, req, certBytes, keyPem); err != nil {
			return nil, err
		}

		// At this point, a forward operation will occur if we're on a standby
		// node as we're now attempting to write the bytes of the cert out to
		// disk.
		if writeCert {
			err = req.Storage.Put(ctx, &logical.StorageEntry{
				Key:   "certs/" + serial,
				Value: certBytes,
			})
			if err != nil {
				return nil, err
			}
		}

		// Finally, we have a valid serial number to use for BYOC revocation!
	}

	// Assumption: this check is cheap. Call this twice, in the cert-import
	// case, to allow cert verification to get rejected on the standby node,
	// but we still need it to protect the serial number case.
	if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) {
		return nil, logical.ErrReadOnly
	}

	// We store and identify by lowercase colon-separated hex, but other
	// utilities use dashes and/or uppercase, so normalize
	serial = strings.ReplaceAll(strings.ToLower(serial), "-", ":")

	b.revokeStorageLock.Lock()
	defer b.revokeStorageLock.Unlock()

	return revokeCert(ctx, b, req, serial, false)
}

func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
	b.revokeStorageLock.RLock()
	defer b.revokeStorageLock.RUnlock()

	crlErr := b.crlBuilder.rebuild(ctx, b, req, false)
	if crlErr != nil {
		switch crlErr.(type) {
		case errutil.UserError:
			return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
		default:
			return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr)
		}
	}

	return &logical.Response{
		Data: map[string]interface{}{
			"success": true,
		},
	}, nil
}

func (b *backend) pathRotateDeltaCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
	sc := b.makeStorageContext(ctx, req.Storage)

	cfg, err := b.crlBuilder.getConfigWithUpdate(sc)
	if err != nil {
		return nil, fmt.Errorf("error fetching CRL configuration: %v", err)
	}

	isEnabled := cfg.EnableDelta

	crlErr := b.crlBuilder.rebuildDeltaCRLsIfForced(sc, true)
	if crlErr != nil {
		switch crlErr.(type) {
		case errutil.UserError:
			return logical.ErrorResponse(fmt.Sprintf("Error during delta CRL building: %s", crlErr)), nil
		default:
			return nil, fmt.Errorf("error encountered during delta CRL building: %w", crlErr)
		}
	}

	resp := &logical.Response{
		Data: map[string]interface{}{
			"success": true,
		},
	}

	if !isEnabled {
		resp.AddWarning("requested rebuild of delta CRL when delta CRL is not enabled; this is a no-op")
	}

	return resp, nil
}

const pathRevokeHelpSyn = `
Revoke a certificate by serial number or with explicit certificate.

When calling /revoke-with-key, the private key corresponding to the
certificate must be provided to authenticate the request.
`

const pathRevokeHelpDesc = `
This allows certificates to be revoke. A root token or corresponding
private key is required.
`

const pathRotateCRLHelpSyn = `
Force a rebuild of the CRL.
`

const pathRotateCRLHelpDesc = `
Force a rebuild of the CRL. This can be used to remove expired certificates from it if no certificates have been revoked. A root token is required.
`

const pathRotateDeltaCRLHelpSyn = `
Force a rebuild of the delta CRL.
`

const pathRotateDeltaCRLHelpDesc = `
Force a rebuild of the delta CRL. This can be used to force an update of the otherwise periodically-rebuilt delta CRLs.
`
