// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

package extensions

import (
	"bytes"
	"context"
	"crypto"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/binary"
	"time"

	"github.com/zeebo/errs"

	"storj.io/common/peertls"
	"storj.io/common/pkcrypto"
)

var (
	// RevocationCheckHandler ensures that a remote peer's certificate chain
	// doesn't contain any revoked certificates.
	RevocationCheckHandler = NewHandlerFactory(&RevocationExtID, revocationChecker)
	// RevocationUpdateHandler looks for certificate revocation extensions on a
	// remote peer's certificate chain, adding them to the revocation DB if valid.
	RevocationUpdateHandler = NewHandlerFactory(&RevocationExtID, revocationUpdater)
)

// ErrRevocation is used when an error occurs involving a certificate revocation.
var ErrRevocation = errs.Class("revocation processing")

// ErrRevocationDB is used when an error occurs involving the revocations database.
var ErrRevocationDB = errs.Class("revocation database")

// ErrRevokedCert is used when a certificate in the chain is revoked and not expected to be.
var ErrRevokedCert = ErrRevocation.New("a certificate in the chain is revoked")

// ErrRevocationTimestamp is used when a revocation's timestamp is older than the last recorded revocation.
var ErrRevocationTimestamp = Error.New("revocation timestamp is older than last known revocation")

// Revocation represents a certificate revocation for storage in the revocation
// database and for use in a TLS extension.
type Revocation struct {
	Timestamp int64
	KeyHash   []byte
	Signature []byte
}

// RevocationDB stores certificate revocation data.
type RevocationDB interface {
	Get(ctx context.Context, chain []*x509.Certificate) (*Revocation, error)
	Put(ctx context.Context, chain []*x509.Certificate, ext pkix.Extension) error
	List(ctx context.Context) ([]*Revocation, error)
}

// NewRevocationExt generates a revocation extension for a certificate.
func NewRevocationExt(key crypto.PrivateKey, revokedCert *x509.Certificate) (pkix.Extension, error) {
	nowUnix := time.Now().Unix()

	keyHash, err := peertls.DoubleSHA256PublicKey(revokedCert.PublicKey)
	if err != nil {
		return pkix.Extension{}, err
	}
	rev := Revocation{
		Timestamp: nowUnix,
		KeyHash:   keyHash[:],
	}

	if err := rev.Sign(key); err != nil {
		return pkix.Extension{}, err
	}

	revBytes, err := rev.Marshal()
	if err != nil {
		return pkix.Extension{}, err
	}

	ext := pkix.Extension{
		Id:    RevocationExtID,
		Value: revBytes,
	}

	return ext, nil
}

func revocationChecker(opts *Options) HandlerFunc {
	return func(_ pkix.Extension, chains [][]*x509.Certificate) error {
		ca, leaf := chains[0][peertls.CAIndex], chains[0][peertls.LeafIndex]
		lastRev, lastRevErr := opts.RevocationDB.Get(context.TODO(), chains[0])
		if lastRevErr != nil {
			return Error.Wrap(lastRevErr)
		}
		if lastRev == nil {
			return nil
		}

		nodeID, err := peertls.DoubleSHA256PublicKey(ca.PublicKey)
		if err != nil {
			return err
		}
		leafKeyHash, err := peertls.DoubleSHA256PublicKey(leaf.PublicKey)
		if err != nil {
			return err
		}

		// NB: we trust that anything that made it into the revocation DB is valid
		//		(i.e. no need for further verification)
		switch {
		case bytes.Equal(lastRev.KeyHash, nodeID[:]):
			fallthrough
		case bytes.Equal(lastRev.KeyHash, leafKeyHash[:]):
			return ErrRevokedCert
		default:
			return nil
		}
	}
}

func revocationUpdater(opts *Options) HandlerFunc {
	return func(ext pkix.Extension, chains [][]*x509.Certificate) error {
		err := opts.RevocationDB.Put(context.TODO(), chains[0], ext)
		return err
	}
}

// Verify checks if the signature of the revocation was produced by the passed cert's public key.
func (r Revocation) Verify(signingCert *x509.Certificate) error {
	pubKey, ok := signingCert.PublicKey.(crypto.PublicKey)
	if !ok {
		return pkcrypto.ErrUnsupportedKey.New("%T", signingCert.PublicKey)
	}

	data := r.TBSBytes()
	err := pkcrypto.HashAndVerifySignature(pubKey, data, r.Signature)
	return err
}

// TBSBytes (ToBeSigned) returns the hash of the revoked certificate key hash
// and the timestamp (i.e. hash(hash(cert bytes) + timestamp)).
func (r *Revocation) TBSBytes() []byte {
	var tsBytes [binary.MaxVarintLen64]byte
	binary.PutVarint(tsBytes[:], r.Timestamp)
	toHash := append(append([]byte{}, r.KeyHash...), tsBytes[:]...)

	return pkcrypto.SHA256Hash(toHash)
}

// Sign generates a signature using the passed key and attaches it to the revocation.
func (r *Revocation) Sign(key crypto.PrivateKey) error {
	data := r.TBSBytes()
	sig, err := pkcrypto.HashAndSign(key, data)
	if err != nil {
		return err
	}
	r.Signature = sig
	return nil
}

// Marshal serializes a revocation to bytes.
func (r Revocation) Marshal() ([]byte, error) {
	return (&revocationEncoder{}).encode(r)
}

// Unmarshal deserializes a revocation from bytes.
func (r *Revocation) Unmarshal(data []byte) error {
	revocation, err := (&revocationDecoder{}).decode(data)
	if err != nil {
		return err
	}
	*r = revocation
	return nil
}
