vbuch / node-signpdf

Simple signing of PDFs in node.
MIT License
696 stars 175 forks source link

PKCS#11 put already signed data to pdf signature #15

Closed ankology closed 4 years ago

ankology commented 5 years ago

Hi Valery! Thank you by the project. I need to sign data using a SmartCard Token, that implements PKCS#11 layer, i already have a working implementation using NodeJS (Migrating from a Java implementation) to sign the data based on some PDF binary, now i need to put this signed data in the PDF, i take a look to your code and i need to ask, to solve my issue, i just need to replace the signed data (generated by node-forge based on .pfx) by my signed data (generated from my current implementation, that already signed with PDF binary and the Token PKCS#11) in code? Thats it? I have spent time to find some PKCS#11 solution to NodeJS but i didn't found pratically nothing, and all PDF libraries have only support to p12|pfx, thank you!

vbuch commented 5 years ago

I cannot at the moment spend much time to research more on it. I don't even know what PKCS11 is. If you look at my code you will see how it works basically. I've tried to explain it in the readme as well. You need to:

  1. have a PDF with a big-enough placeholder
  2. remove the placeholder from the PDF
  3. generate a detached signature of the document
  4. place the signature in the placeholder you previously removed
  5. place the placeholder with the signature back in the PDF That's the flow at least for PPKLite signing. I will try to get back to your question later this week.
ankology commented 5 years ago

Some references: https://en.wikipedia.org/wiki/PKCS_11 https://github.com/PeculiarVentures/pkcs11js (project currently used on my Node implementation) https://docs.oracle.com/javase/8/docs/technotes/guides/security/p11guide.html (used in Java implementation to read PKCS#11 interface and extract the keys etc..) https://www.coderanch.com/how-to/javadoc/itext-2.1.7/com/lowagie/text/pdf/PdfStamper.html (used in Java to manipulate PDF and put the signature)

Basically explained, PKCS#11 is a interface to manipulate the device with cryptograpy data (SmartCards, etc..) and you extract the keys using that interface to sign your data (using this interface to extract the private key to sign), in your implementation you directly need to pass the p12 cert file, and through node-forge you build the signed data.. Actually i have the signed data, and just need to put in PDF. Following the logic you explain, i think the flux is the same, but i don't have any experience in manipulate PDF data like that, today i'm going to code based in your code to find some light, thank you!

Java Code that actually working for both (.p12 and devices that implement PKCS#11):

public class JPdfSign {

    private static final Logger logger = Logger.getLogger(JPdfSign.class.getName());
    private static PrivateKey privateKey;
    private static Certificate[] certificateChain;
    private static final ResourceBundle bundle = ResourceBundle.getBundle("strings");
    private static final String PRODUCTNAME = bundle.getString("productname");
    private static final String VERSION = bundle.getString("version");
    private static final String JAR_FILENAME = bundle.getString("jar-filename");
    private static String PIN;
    private static String reason;
    private static String location;

    public static void main(String[] args) {

        try {

            String pkcs12FileName = args[0].trim();
            String pdfInputFileName = args[1];
            String pdfOutputFileName = args[2];
            PIN = args[3];
            reason = args[4];
            location = args[5];
            boolean usePKCS12 = !(pkcs12FileName.equals("-PKCS11"));

            if (usePKCS12)
                readPrivateKeyFromPKCS12(pkcs12FileName);
            else
                readPrivateKeyFromPKCS11();

            PdfReader reader = null;
            try {

                reader = new PdfReader(pdfInputFileName);

            } catch (IOException e) {

                logger.error("An unknown error accoured while opening the input PDF file: " + pdfInputFileName + " - Error: " + e.getMessage());
            }

            FileOutputStream fout = null;
            try {

                fout = new FileOutputStream(pdfOutputFileName);

            } catch (FileNotFoundException e) {

                logger.error("An unknown error accoured while opening the output PDF file: "+ pdfOutputFileName + " - Error: " + e.getMessage());
            }

            PdfStamper stp = null;
            try {
                stp = PdfStamper.createSignature(reader, fout, '\0');
                stp.setEncryption(true, null, null, PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_SCREENREADERS | PdfWriter.ALLOW_COPY);
                PdfSignatureAppearance sap = stp.getSignatureAppearance();

                sap.setCrypto(privateKey, certificateChain, null, PdfSignatureAppearance.WINCER_SIGNED);

                sap.setReason(reason);
                sap.setLocation(location);
                sap.setContact("contato@gxz.com.br");
                sap.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
                sap.setVisibleSignature(new Rectangle(30, 830, 170, 770), 1, null);

                stp.close();

            } catch (DocumentException | IOException e) {

                logger.error("An unknown error accoured while signing the PDF file: " + e.getMessage());
            }

            fout.close();

        } catch (KeyStoreException kse) {

            logger.error("An unknown error accoured while initializing the KeyStore instance: " + kse.getMessage());
        } catch (IOException ex) {
            logger.error(ex);
        }
    }

    private static void readPrivateKeyFromPKCS11() throws KeyStoreException {

        String DLL = "C:\\Windows\\System32\\aetpkss1.dll";

        String pkcs11ConfigSettings = "name=aet\n"+"library="+DLL;
        byte[] pkcs11ConfigBytes = pkcs11ConfigSettings.getBytes();
        ByteArrayInputStream confStream = new ByteArrayInputStream(pkcs11ConfigBytes);

        SunPKCS11 pkcs11 = new SunPKCS11(confStream);
        Security.addProvider(pkcs11);

        BouncyCastleProvider providerBC = new BouncyCastleProvider();
        Security.addProvider(providerBC);

        KeyStore ks = null;
        try {

            //ks = KeyStore.getInstance("pkcs11", p);
            ks = KeyStore.getInstance("PKCS11");
            ks.load(null, PIN.toCharArray());

        } catch (NoSuchAlgorithmException | IOException e) {

            logger.error("An unknown error accoured while reading the PKCS#11 smartcard: " + e.getMessage());

        } catch (java.security.cert.CertificateException ex) {
            logger.error(ex);
        }

        String alias = "";
        try {

            alias = (String) ks.aliases().nextElement();
            privateKey = (PrivateKey) ks.getKey(alias, PIN.toCharArray());

        } catch (NoSuchElementException e) {

            logger.error("An unknown error accoured while retrieving the private key: " + e.getMessage());
            logger.error("The selected PKCS#12 file does not contain any private keys.");

        } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {

            logger.error("An unknown error accoured while retrieving the private key: " + e.getMessage());
        }

        certificateChain = ks.getCertificateChain(alias);
    }

    protected static void readPrivateKeyFromPKCS12(String pkcs12FileName) throws KeyStoreException {

        KeyStore ks = null;

        try {

            ks = KeyStore.getInstance("pkcs12");
            ks.load(new FileInputStream(pkcs12FileName), PIN.toCharArray());

        } catch (NoSuchAlgorithmException | IOException e) {

            logger.error("An unknown error accoured while reading the PKCS#12 file: " + e.getMessage());

        } catch (java.security.cert.CertificateException ex) {
            logger.error(ex);
        }

        String alias = "";
        try {

            alias = (String) ks.aliases().nextElement();
            privateKey = (PrivateKey) ks.getKey(alias, PIN.toCharArray());

        } catch (NoSuchElementException e) {

            logger.error("An unknown error accoured while retrieving the private key: " + e.getMessage());
            logger.error("The selected PKCS#12 file does not contain any private keys.");

        } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {

            logger.error("An unknown error accoured while retrieving the private key: " + e.getMessage());
        }

        certificateChain = ks.getCertificateChain(alias);
    }

    protected static String getConfigFilePath(String configFilename) {

        CodeSource source = JPdfSign.class.getProtectionDomain().getCodeSource();
        URL url = source.getLocation();

        String jarPath = URLDecoder.decode(url.getFile());
        File f = new File(jarPath);
        try {
            jarPath = f.getCanonicalPath();
        } catch (IOException e) {
        }
        if (!f.isDirectory()) {
            f = new File(jarPath);
            jarPath = f.getParent();
        }
        System.out.println(jarPath);
        if (jarPath.length() > 0) {
            return jarPath + File.separator + configFilename;
        } else
            return configFilename;
    }

}
vbuch commented 5 years ago

Hope you'll be able to sign it. You may need to slightly alter the code of this lib as from your description you already have the crypto-needed data it is not just packed in the PKCS#12 way. We already had an idea (https://github.com/vbuch/node-signpdf/issues/12#issuecomment-425550570) to support other ways of providing signing data so if you have something workable, would love to see it. Let us know if you have any progress and/or issues.

vbuch commented 5 years ago

@ankology did it work out well at the end? Now that I read your latest comment, it seems to me that you only needed to do some basic string manipulations to get it wrapped up. Is that correct? Did you succeed? Was this lib of any help?

ankology commented 5 years ago

@vbuch Hello! I am currently working on other projects, when I come back to work on it and some progress occurs I will tell here!

madanprabhud commented 5 years ago

I'm sorry to wake-up the old thread. We are also working on the solution of PKCS11 - HSM based PDF signing. We used graphene-pk11 for PKCS11 operation and absolutely works fine.

Sample Code to sign.

var graphene = require("graphene-pk11");
var Module = graphene.Module;

var lib = "C:/Windows/System32/SignatureP11.dll";

var mod = Module.load(lib, "PROXKey Module");
mod.initialize();

var slot = mod.getSlots(0);
if (slot.flags & graphene.SlotFlag.TOKEN_PRESENT) {
    var session = slot.open();
    session.login("12345678");

    // generate RSA key pair
    var keys = session.generateKeyPair(graphene.KeyGenMechanism.RSA, {
        keyType: graphene.KeyType.RSA,
        modulusBits: 1024,
        publicExponent: Buffer.from([3]),
        token: false,
        verify: true,
        encrypt: true,
        wrap: true
    }, {
        keyType: graphene.KeyType.RSA,
        token: false,
        sign: true,
        decrypt: true,
        unwrap: true
    });

    // sign content
    var sign = session.createSign("SHA1_RSA_PKCS", keys.privateKey);
    sign.update("simple text 1"); 
    sign.update("simple text 2");
    var signature = sign.final();
    console.log("Signature RSA-SHA1:", signature.toString("hex"));

    session.logout();
    session.close();
}
else {
    console.error("Slot is not initialized");
}
 mod.finalize();

Kindly help me on achieving the same for PDF stream and attaching the PKCS7 into the Final PDF.

vbuch commented 5 years ago

Oh. That's a totally different thing. If you implement it, I imagine it could be reused in this package with two adapters. One is the p12 that this package already has implemented and the p11 would be your code. The tricky part is that reqiures mostly understanding detached signatures. Other than that is should not be much different. You still have the input content (PDF), you need to create a digest of that (https://github.com/vbuch/node-signpdf/blob/master/src/signpdf.js#L139) and then extract the signature for that digest. Actually this is what "signing in detached mode means": The content is not included in the Signature but rather only a digest of the content. How implement this with graphene-pk11 ... I have no clue.

vbuch commented 5 years ago

Oh, man! Now, having a gemalto e-sign, I'm really curious if and how this could work. Whenevr I have time, I would give it a shot to at least experiment with graphene-pk11.

madanprabhud commented 5 years ago

Above solution uses graphene-pk11. Now, I can sign the PDF as follows.,

var graphene = require("graphene-pk11");
var fs = require('fs');
var Module = graphene.Module;

var lib = "C:/Windows/System32/SignatureP11.dll";

var mod = Module.load(lib, "PROXKey Module");
mod.initialize();

var slot = mod.getSlots(0);
if (slot.flags & graphene.SlotFlag.TOKEN_PRESENT) {
    var session = slot.open();
    session.login("Admin123$");

    // generate RSA key pair
    var keys = session.generateKeyPair(graphene.KeyGenMechanism.RSA, {
        keyType: graphene.KeyType.RSA,
        modulusBits: 1024,
        publicExponent: Buffer.from([3]),
        token: false,
        verify: true,
        encrypt: true,
        wrap: true
    }, {
        keyType: graphene.KeyType.RSA,
        token: false,
        sign: true,
        decrypt: true,
        unwrap: true
    });

    //PDF Sign
    let pdf = fs.readFileSync("C:/Users/nic/Desktop/TechGov-Niladri.pdf");
    const lastChar = pdf.slice(pdf.length - 1).toString();
        if (lastChar === '\n') {
            // remove the trailing new line
            pdf = pdf.slice(0, pdf.length - 1);
        }

    // sign content
    var sign = session.createSign("SHA1_RSA_PKCS", keys.privateKey);
    sign.update(pdf.toString('binary'));
    var signature = sign.final();
    console.log("Signature RSA-SHA1:", signature.toString("hex")); 

    // verify content
    var verify = session.createVerify("SHA1_RSA_PKCS", keys.publicKey);
    verify.update(pdf.toString('binary'));
    var verify_result = verify.final(signature);
    console.log("Signature RSA-SHA1 verify:", verify_result);      // Signature RSA-SHA1 verify: true

    session.logout();
    session.close();
}
else {
    console.error("Slot is not initialized");
}
 mod.finalize();

But, I need to attach the certificate and other attributes as you did it using forge. Kindly help me on this.

ankology commented 5 years ago

Hi guys, i'm back this week to the project that uses this functionality but i didn't come to that part in the system .. Basically the app runs in electron, and through javascript i need to sign that PDF file and put the signature on it, today i already have the buffer signed! But i need to attach the signed data to the PDF.

Current code (prototype from past year at november 2018):


let file = $e.target.files[0];
let reader = new FileReader();

reader.onload = function(){

    let fileBuffer = reader.result;

    let pkcs11js = require("pkcs11js");
    let pkcs11 = new pkcs11js.PKCS11();
    pkcs11.load("C:\\Windows\\System32\\aetpkss1.dll");
    console.log(pkcs11);

    pkcs11.C_Initialize();

    try {

        // // Getting info about PKCS11 Module
        // let module_info = pkcs11.C_GetInfo();
        // // console.log(module_info);
        //
        // // Getting list of slots
        // let slots = pkcs11.C_GetSlotList(true);
        // let slot = slots[0];
        //
        // // Getting info about slot
        // let slot_info = pkcs11.C_GetSlotInfo(slot);
        // // Getting info about token
        // let token_info = pkcs11.C_GetTokenInfo(slot);
        // // console.log(slot_info, token_info);
        //
        // // Getting info about Mechanism
        // let mechs = pkcs11.C_GetMechanismList(slot);
        // let mech_info = pkcs11.C_GetMechanismInfo(slot, mechs[0]);
        // console.log(mech_info);

        //Open the session from card
        let session = pkcs11.C_OpenSession(slot, pkcs11js.CKF_RW_SESSION | pkcs11js.CKF_SERIAL_SESSION);

        // Getting info about Session
        // let info = pkcs11.C_GetSessionInfo(session);
        // console.log(info);
        //Log into
        pkcs11.C_Login(session, 1, "PASSWORD FROM SMARTCARD HERE");

        let publicKeyTemplate = [
            { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PUBLIC_KEY },
            { type: pkcs11js.CKA_TOKEN, value: false },
            { type: pkcs11js.CKA_LABEL, value: "My RSA Public Key" },
            { type: pkcs11js.CKA_PUBLIC_EXPONENT, value: new Buffer([1, 0, 1]) },
            { type: pkcs11js.CKA_MODULUS_BITS, value: 2048 },
            { type: pkcs11js.CKA_VERIFY, value: true }
        ];
        let privateKeyTemplate = [
            { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PRIVATE_KEY },
            { type: pkcs11js.CKA_TOKEN, value: false },
            { type: pkcs11js.CKA_LABEL, value: "My RSA Private Key" },
            { type: pkcs11js.CKA_SIGN, value: true },
        ];
        let keys = pkcs11.C_GenerateKeyPair(session, { mechanism: pkcs11js.CKM_RSA_PKCS_KEY_PAIR_GEN }, publicKeyTemplate, privateKeyTemplate);

        pkcs11.C_SignInit(session, { mechanism: pkcs11js.CKM_SHA256_RSA_PKCS }, keys.privateKey);
        console.log(keys.privateKey);
        // pkcs11.C_SignUpdate(session, new Buffer("Incomming message 1"));
        console.log(fileBuffer);
        pkcs11.C_SignUpdate(session, Buffer(fileBuffer));

        //Generated signed data!
        let signature = pkcs11.C_SignFinal(session, Buffer(256));
        console.log(signature, signature.byteLength);

        pkcs11.C_VerifyInit(session, { mechanism: pkcs11js.CKM_SHA256_RSA_PKCS }, keys.publicKey);

        //Check the file signature
        pkcs11.C_VerifyUpdate(session, Buffer(fileBuffer));
        let verify = pkcs11.C_VerifyFinal(session, signature);
        console.log('Verify sign data result:', verify); //Result is bool true!

        pkcs11.C_Logout(session);
        pkcs11.C_CloseSession(session);
    }
    catch(e){
        console.error(e);
    }
    finally {
        pkcs11.C_Finalize();
    }
};

reader.readAsArrayBuffer(file);

So, when i back to code the idea is to put directly these already signed data into the PDF (in its structure placement to signatures) based in your code library what does the same using p12 certificate and that's it!

ankology commented 5 years ago

@vbuch i'm looking the source code and at file/line /src/signpdf.js the method p7.toAsn1() do what in fact? And what is a detached pkcs7 signature in fact? Thanks!

Edit: issue closed unintentionally 😆😆!!

vbuch commented 5 years ago

I may try to explain, but in fact, the idea of this package is to figure it out yourself with a little help in the way. So here come the links that are the "little help".

the method p7.toAsn1() do what in fact?

PKCS7: https://www.npmjs.com/package/node-forge#pkcs7 toAsn1: https://github.com/digitalbazaar/forge/blob/master/lib/pkcs7.js#L155 ASN1: https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One

And what is a detached pkcs7 signature in fact?

https://docs.oracle.com/cd/E19398-01/820-1228/gfnmj/index.html

vbuch commented 4 years ago

No activity. Closing.

amitraky commented 3 years ago

Above solution uses graphene-pk11. Now, I can sign the PDF as follows.,

var graphene = require("graphene-pk11");
var fs = require('fs');
var Module = graphene.Module;

var lib = "C:/Windows/System32/SignatureP11.dll";

var mod = Module.load(lib, "PROXKey Module");
mod.initialize();

var slot = mod.getSlots(0);
if (slot.flags & graphene.SlotFlag.TOKEN_PRESENT) {
    var session = slot.open();
    session.login("Admin123$");

    // generate RSA key pair
    var keys = session.generateKeyPair(graphene.KeyGenMechanism.RSA, {
        keyType: graphene.KeyType.RSA,
        modulusBits: 1024,
        publicExponent: Buffer.from([3]),
        token: false,
        verify: true,
        encrypt: true,
        wrap: true
    }, {
        keyType: graphene.KeyType.RSA,
        token: false,
        sign: true,
        decrypt: true,
        unwrap: true
    });

    //PDF Sign
    let pdf = fs.readFileSync("C:/Users/nic/Desktop/TechGov-Niladri.pdf");
    const lastChar = pdf.slice(pdf.length - 1).toString();
        if (lastChar === '\n') {
            // remove the trailing new line
            pdf = pdf.slice(0, pdf.length - 1);
        }

    // sign content
    var sign = session.createSign("SHA1_RSA_PKCS", keys.privateKey);
    sign.update(pdf.toString('binary'));
    var signature = sign.final();
    console.log("Signature RSA-SHA1:", signature.toString("hex")); 

    // verify content
    var verify = session.createVerify("SHA1_RSA_PKCS", keys.publicKey);
    verify.update(pdf.toString('binary'));
    var verify_result = verify.final(signature);
    console.log("Signature RSA-SHA1 verify:", verify_result);      // Signature RSA-SHA1 verify: true

    session.logout();
    session.close();
}
else {
    console.error("Slot is not initialized");
}
 mod.finalize();

But, I need to attach the certificate and other attributes as you did it using forge. Kindly help me on this.

can we use this code for digital pdf sign any other source where i can find it