/*
 * This example showcases signing a PDF file using a certificate chain and
 * a private key, both extracted from PEM files.
 * The first certificate in the chain is actually used for signing the input
 * PDF file, while the entire chain is embedded in the generated PDF signature
 * in order to validate its authenticity.
 * The example also works if the certificate file contains only the signing
 * certificate.
 *
 * $ ./pdf_sign_pem_multicert <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> <CERTIFICATE_PATH> <PRIVATE_KEY_PATH>
 *
 * Example: ./pdf_sign_pem_multicert in.pdf out.pdf certs.pem key.pem
 */
package main

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"hash"
	"io/ioutil"
	"log"
	"os"
	"time"

	"github.com/unidoc/unipdf/v3/annotator"
	"github.com/unidoc/unipdf/v3/common/license"
	"github.com/unidoc/unipdf/v3/core"
	"github.com/unidoc/unipdf/v3/model"
	"github.com/unidoc/unipdf/v3/model/sighandler"
)

func init() {
	// Make sure to load your metered License API key prior to using the library.
	// If you need a key, you can sign up and create a free one at https://cloud.unidoc.io
	err := license.SetMeteredKey(os.Getenv(`UNIDOC_LICENSE_API_KEY`))
	if err != nil {
		panic(err)
	}
}

const (
	usage = "Usage: %s INPUT_PDF_PATH OUTPUT_PDF_PATH CERTS_PATH PRIVATE_KEY_PATH\n"
)

func main() {
	args := os.Args
	if len(args) < 5 {
		fmt.Printf(usage, os.Args[0])
		return
	}
	inputPath, outputPath := args[1], args[2]
	certPath, privateKeyPath := args[3], args[4]

	// Load signing certificate and certificate chain as a PDF array object.
	// The signing certificate is used to sign the input PDF file.
	// The PDF certificate chain (which includes the signing certificate) will
	// be embedded in the generated PDF signature.
	signingCert, pdfCerts, err := loadCertificates(certPath)
	if err != nil {
		log.Fatal(err)
	}

	// Load private key.
	privateKey, err := loadPrivateKey(privateKeyPath)
	if err != nil {
		log.Fatal(err)
	}

	// Sign input file and write output file.
	err = sign(inputPath, outputPath, signingCert, privateKey, pdfCerts)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("PDF file successfully signed. Output path: %s\n", outputPath)
}

func loadPrivateKey(privateKeyPath string) (*rsa.PrivateKey, error) {
	// Read private key file contents.
	privateKeyData, err := ioutil.ReadFile(privateKeyPath)
	if err != nil {
		log.Fatal(err)
	}

	// Decode PEM block.
	block, _ := pem.Decode(privateKeyData)

	// Parse private key data.
	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	return privateKey, nil
}

func loadCertificates(certPath string) (*x509.Certificate, *core.PdfObjectArray, error) {
	// Read certificate file contents.
	certData, err := ioutil.ReadFile(certPath)
	if err != nil {
		return nil, nil, err
	}

	parseCert := func(data []byte) (*x509.Certificate, []byte, error) {
		// Decode PEM block.
		block, rest := pem.Decode(data)

		// Parse certificate.
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, nil, err
		}

		return cert, rest, nil
	}

	// Create PDF array object which will contain the certificate chain data,
	// loaded from the PEM file. The first element of the array must be the
	// signing certificate. The rest of the certificate chain is used for
	// validating the authenticity of the signing certificate.
	pdfCerts := core.MakeArray()

	// Parse signing certificate.
	signingCert, pemUnparsedData, err := parseCert(certData)
	if err != nil {
		return nil, nil, err
	}
	pdfCerts.Append(core.MakeString(string(signingCert.Raw)))

	// Parse the rest of the certificates contained in the PEM file,
	// if any, and add them to the PDF certificates array.
	for len(pemUnparsedData) != 0 {
		cert, rest, err := parseCert(pemUnparsedData)
		if err != nil {
			return nil, nil, err
		}

		pdfCerts.Append(core.MakeString(string(cert.Raw)))
		pemUnparsedData = rest
	}

	return signingCert, pdfCerts, nil
}

func sign(inputPath, outputPath string, signingCert *x509.Certificate,
	privateKey *rsa.PrivateKey, pdfCerts *core.PdfObjectArray) error {
	// Create signature function.
	signFunc := func(sig *model.PdfSignature, digest model.Hasher) ([]byte, error) {
		h, ok := digest.(hash.Hash)
		if !ok {
			return nil, errors.New("hash type error")
		}

		return privateKey.Sign(rand.Reader, h.Sum(nil), crypto.SHA1)
	}

	// Create custom signature handler.
	handler, err := sighandler.NewAdobeX509RSASHA1Custom(signingCert, signFunc)
	if err != nil {
		return err
	}

	// Create and initialize signature.
	signature := model.NewPdfSignature(handler)
	if err := signature.Initialize(); err != nil {
		return err
	}

	// Set signature fields.
	signature.SetName("Test PEM Multicert Signature")
	signature.SetReason("Test_PEM_Multicert_Signature")
	signature.SetDate(time.Now(), "")

	// Set signature certificate chain.
	signature.Cert = pdfCerts

	// Create signature field and appearance.
	opts := annotator.NewSignatureFieldOpts()
	opts.FontSize = 10
	opts.Rect = []float64{10, 25, 75, 60}

	sigField, err := annotator.NewSignatureField(
		signature,
		[]*annotator.SignatureLine{
			annotator.NewSignatureLine("Name", "John Doe"),
			annotator.NewSignatureLine("Date", "2020.03.27"),
			annotator.NewSignatureLine("Reason", "UniPDF Signature Test"),
		},
		opts,
	)
	sigField.T = core.MakeString("PEM Multicert signature")

	// Open input file.
	file, err := os.Open(inputPath)
	if err != nil {
		log.Fatalf("Fail: %v\n", err)
	}
	defer file.Close()

	// Create reader.
	reader, err := model.NewPdfReader(file)
	if err != nil {
		return err
	}

	// Create appender.
	appender, err := model.NewPdfAppender(reader)
	if err != nil {
		return err
	}

	// Sign input PDF file.
	// The signature appearance is placed on the first page of the output PDF file.
	// See https://github.com/unidoc/unipdf-examples/blob/master/signatures/pdf_sign_appearance.go
	// for adding multiple signature appearance annotations, on multiple pages.
	if err = appender.Sign(1, sigField); err != nil {
		return err
	}

	// Write output file.
	return appender.WriteToFile(outputPath)
}