/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package pkcs11

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"math/big"
	"os"
	"path/filepath"
	"syscall"
	"time"

	bpkcs11 "github.com/hyperledger/fabric/bccsp/pkcs11"
	"github.com/hyperledger/fabric/integration/nwo"
	"github.com/hyperledger/fabric/integration/nwo/fabricconfig"
	"github.com/miekg/pkcs11"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/tedsuo/ifrit"
)

var _ = Describe("PKCS11 enabled network", func() {
	var (
		tempDir   string
		network   *nwo.Network
		chaincode nwo.Chaincode
		process   ifrit.Process
	)

	BeforeEach(func() {
		var err error
		tempDir, err = ioutil.TempDir("", "p11")
		Expect(err).NotTo(HaveOccurred())

		network = nwo.New(nwo.BasicSolo(), tempDir, nil, StartPort(), components)
		network.GenerateConfigTree()
		network.Bootstrap()

		chaincode = nwo.Chaincode{
			Name:            "mycc",
			Version:         "0.0",
			Path:            components.Build("github.com/hyperledger/fabric/integration/chaincode/simple/cmd"),
			Lang:            "binary",
			PackageFile:     filepath.Join(tempDir, "simplecc.tar.gz"),
			Ctor:            `{"Args":["init","a","100","b","200"]}`,
			SignaturePolicy: `AND ('Org1MSP.member','Org2MSP.member')`,
			Sequence:        "1",
			InitRequired:    true,
			Label:           "my_prebuilt_chaincode",
		}
	})

	AfterEach(func() {
		if process != nil {
			process.Signal(syscall.SIGTERM)
			Eventually(process.Wait(), network.EventuallyTimeout).Should(Receive())
		}
		network.Cleanup()
		os.RemoveAll(tempDir)
	})

	Describe("without mapping", func() {
		BeforeEach(func() {
			By("configuring PKCS11 artifacts")
			setupPKCS11(network, noMapping)

			By("starting fabric processes")
			networkRunner := network.NetworkGroupRunner()
			process = ifrit.Invoke(networkRunner)
			Eventually(process.Ready(), network.EventuallyTimeout).Should(BeClosed())
		})

		It("executes transactions against a basic solo network", func() {
			orderer := network.Orderer("orderer")
			network.CreateAndJoinChannels(orderer)

			nwo.EnableCapabilities(network, "testchannel", "Application", "V2_0", orderer, network.PeersWithChannel("testchannel")...)
			nwo.DeployChaincode(network, "testchannel", orderer, chaincode)
			runQueryInvokeQuery(network, orderer, network.Peer("Org1", "peer0"), "testchannel")
		})
	})

	Describe("mapping everything", func() {
		BeforeEach(func() {
			By("configuring PKCS11 artifacts")
			setupPKCS11(network, mapAll)

			By("starting fabric processes")
			networkRunner := network.NetworkGroupRunner()
			process = ifrit.Invoke(networkRunner)
			Eventually(process.Ready(), network.EventuallyTimeout).Should(BeClosed())
		})

		It("executes transactions against a basic solo network", func() {
			orderer := network.Orderer("orderer")
			network.CreateAndJoinChannels(orderer)

			nwo.EnableCapabilities(network, "testchannel", "Application", "V2_0", orderer, network.PeersWithChannel("testchannel")...)
			nwo.DeployChaincode(network, "testchannel", orderer, chaincode)
			runQueryInvokeQuery(network, orderer, network.Peer("Org1", "peer0"), "testchannel")
		})
	})
})

type model uint8

const (
	noMapping = model(iota)
	mapAll
)

func setupPKCS11(network *nwo.Network, model model) {
	lib, pin, label := bpkcs11.FindPKCS11Lib()

	By("establishing a PKCS11 session")
	ctx, sess := setupPKCS11Ctx(lib, label, pin)
	defer ctx.Destroy()
	defer ctx.CloseSession(sess)

	serialNumbers := map[string]*big.Int{}
	configurePeerPKCS11(ctx, sess, network, serialNumbers)
	configureOrdererPKCS11(ctx, sess, network, serialNumbers)

	var keyConfig []fabricconfig.KeyIDMapping
	switch model {
	case noMapping:
	case mapAll:
		updateKeyIdentifiers(ctx, sess, serialNumbers)
		for ski, serial := range serialNumbers {
			keyConfig = append(keyConfig, fabricconfig.KeyIDMapping{
				SKI: ski,
				ID:  serial.String(),
			})
		}
	}

	bccspConfig := &fabricconfig.BCCSP{
		Default: "PKCS11",
		PKCS11: &fabricconfig.PKCS11{
			Security: 256,
			Hash:     "SHA2",
			Pin:      pin,
			Label:    label,
			Library:  lib,
			KeyIDs:   keyConfig,
		},
	}

	By("updating bccsp peer config")
	for _, peer := range network.Peers {
		peerConfig := network.ReadPeerConfig(peer)
		peerConfig.Peer.BCCSP = bccspConfig
		network.WritePeerConfig(peer, peerConfig)
	}

	By("updating bccsp orderer config")
	orderer := network.Orderer("orderer")
	ordererConfig := network.ReadOrdererConfig(orderer)
	ordererConfig.General.BCCSP = bccspConfig
	network.WriteOrdererConfig(orderer, ordererConfig)
}

func configurePeerPKCS11(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, network *nwo.Network, serialNumbers map[string]*big.Int) {
	for _, peer := range network.Peers {
		orgName := peer.Organization

		peerPubKey, peerCSR, peerSerial := createCSR(ctx, sess, orgName, "peer")
		adminPubKey, adminCSR, adminSerial := createCSR(ctx, sess, orgName, "admin")
		userPubKey, userCSR, userSerial := createCSR(ctx, sess, orgName, "client")

		domain := network.Organization(orgName).Domain

		// Retrieves org CA cert
		orgCAPath := network.PeerOrgCADir(network.Organization(orgName))
		caBytes, err := ioutil.ReadFile(filepath.Join(orgCAPath, fmt.Sprintf("ca.%s-cert.pem", domain)))
		Expect(err).NotTo(HaveOccurred())

		By("Updating the peer signcerts")
		newOrdererPemCert := buildCert(caBytes, orgCAPath, peerCSR, peerSerial, peerPubKey)
		updateMSPFolder(network.PeerLocalMSPDir(peer), fmt.Sprintf("peer.%s-cert.pem", domain), newOrdererPemCert)
		serialNumbers[hex.EncodeToString(skiForKey(peerPubKey))] = peerSerial

		By("Updating the peer admin user signcerts")
		newAdminPemCert := buildCert(caBytes, orgCAPath, adminCSR, adminSerial, adminPubKey)
		orgAdminMSPPath := network.PeerUserMSPDir(peer, "Admin")
		updateMSPFolder(orgAdminMSPPath, fmt.Sprintf("Admin@%s-cert.pem", domain), newAdminPemCert)
		serialNumbers[hex.EncodeToString(skiForKey(adminPubKey))] = adminSerial

		By("Updating the peer user1 signcerts")
		newUserPemCert := buildCert(caBytes, orgCAPath, userCSR, userSerial, userPubKey)
		orgUserMSPPath := network.PeerUserMSPDir(peer, "User1")
		updateMSPFolder(orgUserMSPPath, fmt.Sprintf("User1@%s-cert.pem", domain), newUserPemCert)
		serialNumbers[hex.EncodeToString(skiForKey(userPubKey))] = userSerial
	}
}

func configureOrdererPKCS11(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, network *nwo.Network, serialNumbers map[string]*big.Int) {
	orderer := network.Orderer("orderer")
	orgName := orderer.Organization
	domain := network.Organization(orgName).Domain

	ordererPubKey, ordererCSR, ordererSerial := createCSR(ctx, sess, orgName, "orderer")
	adminPubKey, adminCSR, adminSerial := createCSR(ctx, sess, orgName, "admin")

	// Retrieves org CA cert
	orgCAPath := network.OrdererOrgCADir(network.Organization(orgName))
	caBytes, err := ioutil.ReadFile(filepath.Join(orgCAPath, fmt.Sprintf("ca.%s-cert.pem", domain)))
	Expect(err).NotTo(HaveOccurred())

	By("Updating the orderer signcerts")
	newOrdererPemCert := buildCert(caBytes, orgCAPath, ordererCSR, ordererSerial, ordererPubKey)
	updateMSPFolder(network.OrdererLocalMSPDir(orderer), fmt.Sprintf("orderer.%s-cert.pem", domain), newOrdererPemCert)
	serialNumbers[hex.EncodeToString(skiForKey(ordererPubKey))] = ordererSerial

	By("Updating the orderer admin user signcerts")
	newAdminPemCert := buildCert(caBytes, orgCAPath, adminCSR, adminSerial, adminPubKey)
	orgAdminMSPPath := network.OrdererUserMSPDir(orderer, "Admin")
	updateMSPFolder(orgAdminMSPPath, fmt.Sprintf("Admin@%s-cert.pem", domain), newAdminPemCert)
	serialNumbers[hex.EncodeToString(skiForKey(adminPubKey))] = adminSerial
}

// Creates pkcs11 context and session
func setupPKCS11Ctx(lib, label, pin string) (*pkcs11.Ctx, pkcs11.SessionHandle) {
	ctx := pkcs11.New(lib)

	if err := ctx.Initialize(); err != nil {
		Expect(err).To(Equal(pkcs11.Error(pkcs11.CKR_CRYPTOKI_ALREADY_INITIALIZED)))
	} else {
		Expect(err).NotTo(HaveOccurred())
	}

	slot := findPKCS11Slot(ctx, label)
	Expect(slot).Should(BeNumerically(">", 0), "Could not find slot with label %s", label)

	sess, err := ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
	Expect(err).NotTo(HaveOccurred())

	// Login
	err = ctx.Login(sess, pkcs11.CKU_USER, pin)
	Expect(err).NotTo(HaveOccurred())

	return ctx, sess
}

// Identifies pkcs11 slot using specified label
func findPKCS11Slot(ctx *pkcs11.Ctx, label string) uint {
	slots, err := ctx.GetSlotList(true)
	Expect(err).NotTo(HaveOccurred())

	for _, s := range slots {
		tokInfo, err := ctx.GetTokenInfo(s)
		Expect(err).NotTo(HaveOccurred())

		if tokInfo.Label == label {
			return s
		}
	}

	return 0
}

// Creates CSR for provided organization and organizational unit
func createCSR(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, org, ou string) (*ecdsa.PublicKey, *x509.CertificateRequest, *big.Int) {
	pubKey, pkcs11Key := generateKeyPair(ctx, sess)

	csrTemplate := x509.CertificateRequest{
		Subject: pkix.Name{
			Country:            []string{"US"},
			Province:           []string{"California"},
			Locality:           []string{"San Francisco"},
			Organization:       []string{fmt.Sprintf("%s.example.com", org)},
			OrganizationalUnit: []string{ou},
			CommonName:         fmt.Sprintf("peer.%s.example.com", org),
		},
		SignatureAlgorithm: x509.ECDSAWithSHA256,
	}

	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, pkcs11Key)
	Expect(err).NotTo(HaveOccurred())

	csr, err := x509.ParseCertificateRequest(csrBytes)
	Expect(err).NotTo(HaveOccurred())
	err = csr.CheckSignature()
	Expect(err).NotTo(HaveOccurred())

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	Expect(err).NotTo(HaveOccurred())

	return pubKey, csr, serialNumber
}

func buildCert(caBytes []byte, org1CAPath string, csr *x509.CertificateRequest, serialNumber *big.Int, pubKey *ecdsa.PublicKey) []byte {
	pemBlock, _ := pem.Decode(caBytes)
	Expect(pemBlock).NotTo(BeNil())

	caCert, err := x509.ParseCertificate(pemBlock.Bytes)
	Expect(err).NotTo(HaveOccurred())

	keyBytes, err := ioutil.ReadFile(filepath.Join(org1CAPath, "priv_sk"))
	Expect(err).NotTo(HaveOccurred())

	pemBlock, _ = pem.Decode(keyBytes)
	Expect(pemBlock).NotTo(BeNil())
	key, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
	Expect(err).NotTo(HaveOccurred())
	caKey := key.(*ecdsa.PrivateKey)

	certTemplate := &x509.Certificate{
		Signature:          csr.Signature,
		SignatureAlgorithm: csr.SignatureAlgorithm,
		PublicKey:          csr.PublicKey,
		PublicKeyAlgorithm: csr.PublicKeyAlgorithm,

		SerialNumber:          serialNumber,
		NotBefore:             time.Now().Add(-1 * time.Minute).UTC(),
		NotAfter:              time.Now().Add(365 * 24 * time.Hour).UTC(),
		BasicConstraintsValid: true,

		Subject:     csr.Subject,
		KeyUsage:    x509.KeyUsageDigitalSignature,
		ExtKeyUsage: []x509.ExtKeyUsage{},
	}

	// Use root CA to create and sign cert
	signedCert, err := x509.CreateCertificate(rand.Reader, certTemplate, caCert, pubKey, caKey)
	Expect(err).NotTo(HaveOccurred())

	return pem.EncodeToMemory(&pem.Block{Bytes: signedCert, Type: "CERTIFICATE"})
}

// Overwrites existing cert and removes private key from keystore folder
func updateMSPFolder(path, certName string, cert []byte) {
	// Overwrite existing certificate with new certificate
	err := ioutil.WriteFile(filepath.Join(path, "signcerts", certName), cert, 0644)
	Expect(err).NotTo(HaveOccurred())

	// delete the existing private key - this is stored in the hsm
	adminKSCert := filepath.Join(path, "keystore", "priv_sk")
	err = os.Remove(adminKSCert)
	Expect(err).NotTo(HaveOccurred())
}

// Generating key pair in HSM, convert, and return keys
func generateKeyPair(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle) (*ecdsa.PublicKey, *P11ECDSAKey) {
	publabel, privlabel := "BCPUB7", "BCPRV7"

	curve := asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} // secp256r1 Curve

	marshaledOID, err := asn1.Marshal(curve)
	Expect(err).NotTo(HaveOccurred())

	pubAttrs := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC),
		pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY),
		pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
		pkcs11.NewAttribute(pkcs11.CKA_VERIFY, true),
		pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, marshaledOID),

		pkcs11.NewAttribute(pkcs11.CKA_ID, publabel),
		pkcs11.NewAttribute(pkcs11.CKA_LABEL, publabel),
	}

	privAttrs := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC),
		pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
		pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
		pkcs11.NewAttribute(pkcs11.CKA_SIGN, true),

		pkcs11.NewAttribute(pkcs11.CKA_ID, privlabel),
		pkcs11.NewAttribute(pkcs11.CKA_LABEL, privlabel),

		pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, false),
		pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, true),
	}

	pubK, privK, err := ctx.GenerateKeyPair(
		sess,
		[]*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_EC_KEY_PAIR_GEN, nil)},
		pubAttrs,
		privAttrs,
	)
	Expect(err).NotTo(HaveOccurred())

	ecpt := ecPoint(ctx, sess, pubK)
	Expect(ecpt).NotTo(BeEmpty(), "CKA_EC_POINT not found")

	hash := sha256.Sum256(ecpt)
	ski := hash[:]

	setskiT := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_ID, ski),
		pkcs11.NewAttribute(pkcs11.CKA_LABEL, hex.EncodeToString(ski)),
	}

	err = ctx.SetAttributeValue(sess, pubK, setskiT)
	Expect(err).NotTo(HaveOccurred())

	err = ctx.SetAttributeValue(sess, privK, setskiT)
	Expect(err).NotTo(HaveOccurred())

	// convert pub key to ansi types
	nistCurve := elliptic.P256()
	x, y := elliptic.Unmarshal(nistCurve, ecpt)
	if x == nil {
		Expect(x).NotTo(BeNil(), "Failed Unmarshaling Public Key")
	}

	pubKey := &ecdsa.PublicKey{Curve: nistCurve, X: x, Y: y}

	pkcs11Key := &P11ECDSAKey{
		ctx:              ctx,
		session:          sess,
		publicKey:        pubKey,
		privateKeyHandle: privK,
	}

	return pubKey, pkcs11Key
}

// SoftHSM reports extra two bytes before the uncompressed point
// see /bccsp/pkcs11/pkcs11.go::ecPoint() for additional details
func ecPoint(pkcs11lib *pkcs11.Ctx, session pkcs11.SessionHandle, key pkcs11.ObjectHandle) (ecpt []byte) {
	template := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, nil),
		pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, nil),
	}

	attr, err := pkcs11lib.GetAttributeValue(session, key, template)
	if err != nil {
		Expect(err).NotTo(HaveOccurred(), "PKCS11: get(EC point)")
	}

	for _, a := range attr {
		if a.Type != pkcs11.CKA_EC_POINT {
			continue
		}

		switch {
		case ((len(a.Value) % 2) == 0) && (byte(0x04) == a.Value[0]) && (byte(0x04) == a.Value[len(a.Value)-1]):
			ecpt = a.Value[0 : len(a.Value)-1] // Trim trailing 0x04
		case byte(0x04) == a.Value[0] && byte(0x04) == a.Value[2]:
			ecpt = a.Value[2:len(a.Value)]
		default:
			ecpt = a.Value
		}
	}

	return ecpt
}

func skiForKey(pk *ecdsa.PublicKey) []byte {
	ski := sha256.Sum256(elliptic.Marshal(pk.Curve, pk.X, pk.Y))
	return ski[:]
}

func updateKeyIdentifiers(pctx *pkcs11.Ctx, sess pkcs11.SessionHandle, serialNumbers map[string]*big.Int) {
	for ks, serial := range serialNumbers {
		ski, err := hex.DecodeString(ks)
		Expect(err).NotTo(HaveOccurred())

		updateKeyIdentifier(pctx, sess, pkcs11.CKO_PUBLIC_KEY, ski, []byte(serial.String()))
		updateKeyIdentifier(pctx, sess, pkcs11.CKO_PRIVATE_KEY, ski, []byte(serial.String()))
	}
}

func updateKeyIdentifier(pctx *pkcs11.Ctx, sess pkcs11.SessionHandle, class uint, currentID, newID []byte) {
	pkt := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_CLASS, class),
		pkcs11.NewAttribute(pkcs11.CKA_ID, currentID),
	}
	err := pctx.FindObjectsInit(sess, pkt)
	Expect(err).NotTo(HaveOccurred())

	objs, _, err := pctx.FindObjects(sess, 1)
	Expect(err).NotTo(HaveOccurred())
	Expect(objs).To(HaveLen(1))

	err = pctx.FindObjectsFinal(sess)
	Expect(err).NotTo(HaveOccurred())

	err = pctx.SetAttributeValue(sess, objs[0], []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_ID, newID),
	})
	Expect(err).NotTo(HaveOccurred())
}
