/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package it.tnx.invoicex.fe.aruba;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.security.GeneralSecurityException;
import java.security.KeyStore;

import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.ess.ESSCertIDv2;
import org.bouncycastle.asn1.ess.SigningCertificateV2;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSAttributeTableGenerator;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator;
import org.bouncycastle.cms.SignerInfoGenerator;
import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.x509.X509CollectionStoreParameters;

/**
 *
 * @author mcecc
 */
public class TestSign {

    private static final String PKCS11_KEYSTORE_TYPE = "PKCS11";
    private static final String X509_CERTIFICATE_TYPE = "X.509";
    private static final String CERTIFICATION_CHAIN_ENCODING = "PkiPath";
    private static final String DIGITAL_SIGNATURE_ALGORITHM_NAME = "SHA1withRSA";
    private static final String SUN_PKCS11_PROVIDER_CLASS = "sun.security.pkcs11.SunPKCS11";

    private static final String CERT_CHAIN_FIELD_PARAM = "certificationChainField";
    private static final String SIGNATURE_FIELD_PARAM = "signatureField";

    public static String fileName = "f:\\5448.pdf";

    public static void main(String[] args) {
        try {
            // Get the file name to be signed from the form in the HTML document

            Security.addProvider(new BouncyCastleProvider());

            // Perform the actual file signing
            CertificationChainAndSignatureBase64 signingResult = signFile(fileName);
            System.out.println("signingResult = " + signingResult);
            if (signingResult != null) {
                // Document  signed. Fill the certificate and signature fields

                System.out.println(signingResult.mCertificationChain);
                System.out.println(signingResult.mSignature);
            } else {
                // User canceled signing
                System.out.println("canceled");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Signs given local file. The certificate and private key to be used for
     * signing come from the locally attached smart card. The user is requested
     * to provide a PKCS#11 implementation library and the PIN code for
     * accessing the smart card.
     *
     * @param aFileName the name of the file to be signed.
     * @return the digital signature of the given file and the certification
     * chain of the certificatie used for signing the file, both Base64-encoded
     * or null if the signing process is canceled by the user.
     * @throws DocumentSignException when a problem arised during the singing
     * process (e.g. smart card access problem, invalid certificate, invalid PIN
     * code, etc.)
     */
    static private CertificationChainAndSignatureBase64 signFile(String aFileName)
            throws DocumentSignException, NoSuchAlgorithmException, CertificateEncodingException, OperatorCreationException, IOException, CMSException {

        // Load the file for signing
        byte[] documentToSign = null;
        try {
            documentToSign = readFileInByteArray(aFileName);
        } catch (IOException ioex) {
            String errorMessage = "Can not read the file for signing " + aFileName + ".";
            throw new DocumentSignException(errorMessage, ioex);
        }

        // Show a dialog for choosing PKCS#11 implementation library and smart card PIN
        PKCS11LibraryFileAndPINCodeDialog pkcs11Dialog = new PKCS11LibraryFileAndPINCodeDialog();
        boolean dialogConfirmed;
        try {
            dialogConfirmed = pkcs11Dialog.run();
        } finally {
            pkcs11Dialog.dispose();
        }

        if (dialogConfirmed) {
//            String oldButtonLabel = mSignButton.getLabel();
//            mSignButton.setLabel("Working...");
//            mSignButton.setEnabled(false);
            try {
                String pkcs11LibraryFileName = pkcs11Dialog.getLibraryFileName();
                String pinCode = pkcs11Dialog.getSmartCardPINCode();

                // Do the actual signing of the document with the smart card
                CertificationChainAndSignatureBase64 signingResult = signDocument(documentToSign, pkcs11LibraryFileName, pinCode);
                return signingResult;
            } finally {
//                mSignButton.setLabel(oldButtonLabel);
//                mSignButton.setEnabled(true);
            }
        } else {
            return null;
        }
    }

    static private CertificationChainAndSignatureBase64 signDocument(
            byte[] aDocumentToSign, String aPkcs11LibraryFileName, String aPinCode)
            throws DocumentSignException, NoSuchAlgorithmException, CertificateEncodingException, OperatorCreationException, IOException, CMSException {
        if (aPkcs11LibraryFileName.length() == 0) {
            String errorMessage = "It is mandatory to choose a PCKS#11 native "
                    + "implementation library for for smart card (.dll or .so file)!";
            throw new DocumentSignException(errorMessage);
        }

        // Load the keystore from the smart card using the specified PIN code
        KeyStore userKeyStore = null;
        try {
            userKeyStore = loadKeyStoreFromSmartCard(aPkcs11LibraryFileName, aPinCode);
        } catch (Exception ex) {
            String errorMessage = "Can not read the keystore from the smart card.\n"
                    + "Possible reasons:\n"
                    + " - The smart card reader in not connected.\n"
                    + " - The smart card is not inserted.\n"
                    + " - The PKCS#11 implementation library is invalid.\n"
                    + " - The PIN for the smart card is incorrect.\n"
                    + "Problem details: " + ex.getMessage();
            throw new DocumentSignException(errorMessage, ex);
        }

        // Get the private key and its certification chain from the keystore
        PrivateKeyAndCertChain privateKeyAndCertChain = null;
        try {
            privateKeyAndCertChain = getPrivateKeyAndCertChain(userKeyStore);
        } catch (GeneralSecurityException gsex) {
            String errorMessage = "Can not extract the private key and "
                    + "certificate from the smart card. Reason: " + gsex.getMessage();
            throw new DocumentSignException(errorMessage, gsex);
        }

        // Check if the private key is available
        PrivateKey privateKey = privateKeyAndCertChain.mPrivateKey;
        if (privateKey == null) {
            String errorMessage = "Can not find the private key on the smart card.";
            throw new DocumentSignException(errorMessage);
        }

        // Check if X.509 certification chain is available
        Certificate[] certChain = privateKeyAndCertChain.mCertificationChain;
        if (certChain == null) {
            String errorMessage = "Can not find the certificate on the smart card.";
            throw new DocumentSignException(errorMessage);
        }

        // Create the result object
        CertificationChainAndSignatureBase64 signingResult = new CertificationChainAndSignatureBase64();

        // Save X.509 certification chain in the result encoded in Base64
        try {
            signingResult.mCertificationChain = encodeX509CertChainToBase64(certChain);
        } catch (CertificateException cee) {
            String errorMessage = "Invalid certificate on the smart card.";
            throw new DocumentSignException(errorMessage);
        }

        // Calculate the digital signature of the file,
        // encode it in Base64 and save it in the result
        try {
            byte[] digitalSignature = signDocument(aDocumentToSign, privateKey);
            signingResult.mSignature = Base64Utils.base64Encode(digitalSignature);
        } catch (GeneralSecurityException gsex) {
            String errorMessage = "File signing failed.\n"
                    + "Problem details: " + gsex.getMessage();
            throw new DocumentSignException(errorMessage, gsex);
        }

        //P7M - bouncy castle - http://www.netlogo.net/Netlogo/page/programmazione/java/firma-digitale-il-nuovo-formato-cades.php
        Certificate cert = certChain[0];
        String digestAlgorithm = "SHA-256";
        String digitalSignatureAlgorithmName = "SHA256withRSA";
        MessageDigest sha = MessageDigest.getInstance(digestAlgorithm);
        System.out.println(cert.toString());
        byte[] digestedCert = sha.digest(cert.getEncoded());

        AlgorithmIdentifier aiSha256 = new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256);
        ESSCertIDv2 essCert1 = new ESSCertIDv2(aiSha256, digestedCert);
        ESSCertIDv2[] essCert1Arr = {essCert1};
        SigningCertificateV2 scv2 = new SigningCertificateV2(essCert1Arr);
        Attribute certHAttribute = new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(scv2));

        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(certHAttribute);
        AttributeTable at = new AttributeTable(v);
        CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(at);

        SignerInfoGeneratorBuilder genBuild = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider());
        genBuild.setSignedAttributeGenerator(attrGen);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        ContentSigner shaSigner = new JcaContentSignerBuilder("SHA256withRSA").build(privateKeyAndCertChain.mPrivateKey);
        SignerInfoGenerator sifGen = genBuild.build(shaSigner, new X509CertificateHolder(cert.getEncoded()));
        gen.addSignerInfoGenerator(sifGen);

        ArrayList<X509Certificate> certsin = new ArrayList<X509Certificate>();
        for (int i = 0; i < certChain.length; i++) {
            certsin.add((X509Certificate) certChain[i]);
        }
        X509CollectionStoreParameters x509CollectionStoreParameters = new X509CollectionStoreParameters(certsin);

        JcaCertStore jcaCertStore = new JcaCertStore(certsin);
        gen.addCertificates(jcaCertStore);

        byte[] plainfile = readFileInByteArray(fileName);
        CMSTypedData msg = new CMSProcessableByteArray(plainfile);
        CMSSignedData sigData = gen.generate(msg, true);
        byte[] encoded = sigData.getEncoded();
        FileOutputStream fos = new FileOutputStream(fileName + "-cecca.p7m");
        fos.write(encoded);
        fos.flush();
        fos.close();

        return signingResult;
    }

    /**
     * Loads the keystore from the smart card using its PKCS#11 implementation
     * library and the Sun PKCS#11 security provider. The PIN code for accessing
     * the smart card is required.
     */
    static private KeyStore loadKeyStoreFromSmartCard(String aPKCS11LibraryFileName,
            String aSmartCardPIN)
            throws GeneralSecurityException, IOException {
        // First configure the Sun PKCS#11 provider. It requires a stream (or file)
        // containing the configuration parameters - "name" and "library".
        String pkcs11ConfigSettings = "name = SmartCard\n" + "library = " + aPKCS11LibraryFileName;
        byte[] pkcs11ConfigBytes = pkcs11ConfigSettings.getBytes();
        ByteArrayInputStream confStream = new ByteArrayInputStream(pkcs11ConfigBytes);

        // Instantiate the provider dynamically with Java reflection
        try {
            Class sunPkcs11Class = Class.forName(SUN_PKCS11_PROVIDER_CLASS);
            Constructor pkcs11Constr = sunPkcs11Class.getConstructor(
                    java.io.InputStream.class);
            Provider pkcs11Provider = (Provider) pkcs11Constr.newInstance(confStream);
            Security.addProvider(pkcs11Provider);
        } catch (Exception e) {
            throw new KeyStoreException("Cannot initialize Sun PKCS#11 security "
                    + "provider. Reason: " + e.getCause().getMessage());
        }

        // Read the keystore form the smart card
        char[] pin = aSmartCardPIN.toCharArray();
        KeyStore keyStore = KeyStore.getInstance(PKCS11_KEYSTORE_TYPE);
        keyStore.load(null, pin);
        return keyStore;
    }

    /**
     * @return private key and certification chain corresponding to it,
     * extracted from given keystore. The keystore is considered to have only
     * one entry that contains both certification chain and its corresponding
     * private key. If the keystore has no entries, an exception is thrown.
     */
    static private PrivateKeyAndCertChain getPrivateKeyAndCertChain(
            KeyStore aKeyStore)
            throws GeneralSecurityException {
        Enumeration aliasesEnum = aKeyStore.aliases();
        if (aliasesEnum.hasMoreElements()) {
            String alias = (String) aliasesEnum.nextElement();
            Certificate[] certificationChain = aKeyStore.getCertificateChain(alias);
            PrivateKey privateKey = (PrivateKey) aKeyStore.getKey(alias, null);
            PrivateKeyAndCertChain result = new PrivateKeyAndCertChain();
            result.mPrivateKey = privateKey;
            result.mCertificationChain = certificationChain;
            return result;
        } else {
            throw new KeyStoreException("The keystore is empty!");
        }
    }

    /**
     * @return Base64-encoded ASN.1 DER representation of given X.509
     * certification chain.
     */
    static private String encodeX509CertChainToBase64(Certificate[] aCertificationChain)
            throws CertificateException {
        List certList = Arrays.asList(aCertificationChain);
        CertificateFactory certFactory
                = CertificateFactory.getInstance(X509_CERTIFICATE_TYPE);
        CertPath certPath = certFactory.generateCertPath(certList);
        byte[] certPathEncoded = certPath.getEncoded(CERTIFICATION_CHAIN_ENCODING);
        String base64encodedCertChain = Base64Utils.base64Encode(certPathEncoded);
        return base64encodedCertChain;
    }

    /**
     * Reads the specified file into a byte array.
     */
    static private byte[] readFileInByteArray(String aFileName)
            throws IOException {
        File file = new File(aFileName);
        FileInputStream fileStream = new FileInputStream(file);
        try {
            int fileSize = (int) file.length();
            byte[] data = new byte[fileSize];
            int bytesRead = 0;
            while (bytesRead < fileSize) {
                bytesRead += fileStream.read(data, bytesRead, fileSize - bytesRead);
            }
            return data;
        } finally {
            fileStream.close();
        }
    }

    /**
     * Signs given document with a given private key.
     */
    static private byte[] signDocument(byte[] aDocument, PrivateKey aPrivateKey)
            throws GeneralSecurityException {
        Signature signatureAlgorithm
                = Signature.getInstance(DIGITAL_SIGNATURE_ALGORITHM_NAME);
        signatureAlgorithm.initSign(aPrivateKey);
        signatureAlgorithm.update(aDocument);
        byte[] digitalSignature = signatureAlgorithm.sign();
        return digitalSignature;
    }

    /**
     * Data structure that holds a pair of private key and certification chain
     * corresponding to this private key.
     */
    static class PrivateKeyAndCertChain {

        public PrivateKey mPrivateKey;
        public Certificate[] mCertificationChain;
    }

    /**
     * Data structure that holds a pair of Base64-encoded certification chain
     * and digital signature.
     */
    static class CertificationChainAndSignatureBase64 {

        public String mCertificationChain = null;
        public String mSignature = null;
    }

    /**
     * Exception class used for document signing errors.
     */
    static class DocumentSignException extends Exception {

        public DocumentSignException(String aMessage) {
            super(aMessage);
        }

        public DocumentSignException(String aMessage, Throwable aCause) {
            super(aMessage, aCause);
        }
    }
}
