Saturday, December 8, 2007

Getting Started with iText - How to sign a PDF using iText

After a comment from Web Specialist on certificates, I decided to investigate how to sign a PDF using iText, using the java example by Paul Soares.

Unlike the previous examples, this one uses Mark Mandel's JavaLoader.cfc to load the iText-2.0.7.jar which is more recent than the one that ships with MX7 or CF8. The JavaLoader.cfc is an amazingly helpful tool, but if you are using MX7 this article is a must read: Using a Java URLClassLoader in CFMX Can Cause a Memory Leak


UPDATE: I may not have emphasized the point as strongly as I should have. But in summary it is recommended that you store a single instance of the javaLoader in the server scope, rather than creating a new object each time (as in the code below). This prevents a memory leak caused by a bug in ColdFusion MX. I do not know if this bug exists in ColdFusion 8.



There is always a catch ..
Translating the example went smoothly at first, but I soon encountered three problems. The first issue involved the java code's use of a null character. Now according to the API, the third parameter in the createSignature() method must be a java char.


PdfStamper stp = PdfStamper.createSignature(reader, fout, '\0');


Unfortunately, the JavaCast() function in MX7 does not support the char type. So my first thought was to use the java.lang.String.charAt(int) method to convert '\0' to a character. But it turns out that '\0' was treated as two separate characters: '\' and '0' and not a null character. Finally I stumbled on a tip that seemed to work: Using NULL characters in a ColdFusion string.

The next problem raised its ugly head when it came time to make the signature visible.


// Java code
// comment next line to have an invisible signature
sap.setVisibleSignature(new Rectangle(100, 100, 200, 200), 1, null);

// Problem ColdFusion code
rectangle = createObject("java", "com.lowagie.text.Rectangle").init(
javacast("float", 100),
javacast("float", 100),
javacast("float", 200),
javacast("float", 200) );
signatureAppearance.setVisibleSignature( rectangle,
javacast("int", 1), javacast("null", "") );



The error that occurred is rather puzzling. I have re-checked the API, reviewed the iText class bytecode and cfdump'd the signature object. As far as I can tell, everything suggests that the CF code is calling the setVisibleSignature() method correctly. But despite all that ColdFusion insists on responding with the message below. After trying several different things, I decided to keep the signature invisible and move on.

The selected method setVisibleSignature was not found.

Either there are no methods with the specified method name and argument types, or the method setVisibleSignature is overloaded with arguments types that ColdFusion can't decipher reliably. If this is a Java object and you verified
that the method exists, you may need to use the javacast function to reduce
ambiguity.


The final error occurred when attempting to close the PDFStamper object.

500 org/bouncycastle/asn1/DEREncodable
org/bouncycastle/asn1/DEREncodable



Not knowing much about the error, other than it involved one of the bouncycastle classes, I checked the exception.log. It showed the full error message was java.lang.NoClassDefFoundError: org/bouncycastle/asn1/DEREncodable. A quick google turned up a suggestion by Michael Schierl that placing the bcprov-jdk14-135.jar in the class path might fix that error. So I downloaded the bcprov-jdk14-135.jar from SVN and loaded it with the JavaLoader and .. voila! It worked.

Are you ever going to get to the code?
I will continue to investigate how to make the signature visible. But for now here is a working example (mostly) of how to sign a PDF using iText. Since this is my first experience using certificates with iText, comments/suggestions/corrections are welcome.

You do not belong. You are not one of us!


Update: I figured out how to make the signature visible. It was an error in my code. The original code created a rectangle object using createObject() when it should have been created with javaLoader.create().



// cfSearching: Commented out because I have not discovered how to make this method work.
// cfSearching: It causes an excepetion "The selected method setVisibleSignature was not found."
rectangle = createObject("java", "com.lowagie.text.Rectangle").init(
javacast("float", 100),
javacast("float", 100),
javacast("float", 200),
javacast("float", 200) );

signatureAppearance.setVisibleSignature( rectangle,
javacast("int", 1), javacast("null", "") );


Why does this matter? It seems that the setVisibleSignature method is smart enough to recognize that while it was passed some type of Rectangle object, it not a Rectangle object from its own jar. At least that is what seems to be happening here. Mystery solved. I have updated the code to reflect the correct syntax.


Source: How to sign a PDF using iText


<h1>How to sign a PDF using iText</h1>
<cfscript>
fullPathToPFXFile = ExpandPath('./MyPFXFile.pfx');
fullPathToOriginalFile = ExpandPath("./HelloWorld.pdf");
fullPathToSignedFile = ExpandPath("./HelloWorldSigned.pdf");

// cfSearching: My iText and bouncycastle jars are in the same directory
// cfSearching: as the test .CFM page. Your paths may be different
dotNotationPathToJavaLoader = "com.javaloader.JavaLoader";
fullPathToITextJar = ExpandPath("./iText-2.0.7.jar");
fullPathToExtraJar = ExpandPath("./bcprov-jdk14-138.jar");

string = createObject("java","java.lang.String");
myPassword = string.init("MyRealPasswordHere");

pathsForJavaLoader = arrayNew(1);
arrayAppend(pathsForJavaLoader, fullPathToITextJar);
arrayAppend(pathsForJavaLoader, fullPathToExtraJar);

// cfSearching: Important: See "Using a Java URLClassLoader in CFMX Can Cause a Memory Leak"
// cfSearching: http://www.compoundtheory.com/?action=displayPost&ID=212
javaLoader = createObject('component', dotNotationPathToJavaLoader).init(pathsForJavaLoader);

keyStore = createObject("java","java.security.KeyStore").getInstance("pkcs12");
privateKey = createObject("java","java.security.PrivateKey");
inputStream = createObject("java","java.io.FileInputStream").init( fullPathToPFXFile ) ;
keyStore.load( inputStream, myPassword.toCharArray() );
keyStoreAlias = keyStore.aliases().nextElement();
privateKey = keyStore.getKey( keyStoreAlias, myPassword.toCharArray());
chainArray = keyStore.getCertificateChain( keyStoreAlias );

pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init( fullPathToOriginalFile );
outStream = createObject("java","java.io.FileOutputStream").init( fullPathToSignedFile );
pdfStamper = javaLoader.create("com.lowagie.text.pdf.PdfStamper");
// cfSearching: There should NOT be spaces in between % 0 and 0
// cfSearching: The spaces are to force the blogger to display the code correctly
pdfVersion = string.init(URLDecode('% 0 0')).charAt(0);
stamper = pdfStamper.createSignature(pdfReader, outStream, pdfVersion);

signatureAppearance = stamper.getSignatureAppearance();
PdfSignatureAppearance = javaLoader.create("com.lowagie.text.pdf.PdfSignatureAppearance");
signatureAppearance.setCrypto( privateKey, chainArray, javacast("null", 0),
PdfSignatureAppearance.WINCER_SIGNED);

signatureAppearance.setReason("I'm the author");
signatureAppearance.setLocation("Lisbon");

// cfSearching: Commented out because I have not discovered how to make this method work.
// cfSearching: It causes an excepetion "The selected method setVisibleSignature was not found."

rectangle = javaLoader.create("com.lowagie.text.Rectangle").init(
javacast("float", 100),
javacast("float", 100),
javacast("float", 200),
javacast("float", 200) );
signatureAppearance.setVisibleSignature( rectangle, javacast("int", 1), javacast("null", "") );


stamper.close();
outStream.close();
</cfscript>

10 comments:

Web Specialist December 8, 2007 at 9:08 AM  

After try your example I found the null error:
String index out of range: 0

referenced by
196 : pdfVersion = string.init(URLDecode('')).charAt(0);

What do you suggest to solve this? I'm using CF7.

cfSearching December 8, 2007 at 12:22 PM  

The blogger is distorting the code. Inside the URLDecode('') function should be three characters. A percent sign followed by two zeros.

URLDecode('% 0 0')

Web Specialist December 8, 2007 at 1:39 PM  

Wonderful! Right now I'll play with certification following Paulo Soares tutorial(http://itextpdf.sourceforge.net/howtosign.html#howtocertify). Thanks for your time and iText knowledge.
Marco Antonio

cfSearching December 8, 2007 at 2:43 PM  

You're welcome. If you run into any other problems, or find any other cool examples of things you can do with iText or other java classes, let me know!

cfSearching December 11, 2007 at 12:16 AM  

Marco,

I found the problem with setVisibleSignature and updated the code.

HTH

Anonymous,  December 20, 2007 at 3:48 AM  

Only a tip: Mark Mandel(javaloader creator) recommends to instantiate javaloader in a server scope, ok? Please look this: http://www.compoundtheory.com/?action=displayPost&ID=212

Marco Antonio

cfSearching December 20, 2007 at 10:55 AM  

Yes, you are correct. I mentioned that in the code comments above and the instructions on using the JavaLoader.cfc with iText here

http://cfsearching.blogspot.com/2007/12/how-to-use-newer-version-of-itext.html

Mark also mentions a fix for Transfer that automatically puts the javaloader in the server scope. I did not think it applied to the stand-alone version of the cfc. But then I recently noticed a new key in the server scope for the javaloader. So I am wondering if the fix applies here as well. I will check it out and post my findings.

cfSearching December 21, 2007 at 12:00 AM  

I asked about this on http://www.compoundtheory.com and my understanding is the stand-alone javaLoader.cfc must still be stored in the server scope to prevent memory leaks.

Obviously I did not emphasize the link strongly enough, which may have led to some confusion. So I will be updating the entries to reflect this.

Thanks for bringing it to my attention :)

Webpointz September 1, 2009 at 10:40 AM  

I have a SIGNED PDF file that has the physical Entrust DS in the document.

How can I programmatically see the contents in the signature field to ascertain some information as to who signed it?

Documentation on this, especially with CFMX is not out there.

Thanks

Unknown March 11, 2010 at 12:43 PM  

how about a signature with an image?

I got an image to work, but i goes on top of the signature type.

sig = ExpandPath("sig.jpg");
// create image object
image = createObject("java","com.lowagie.text.Image");
signature = image.getInstance( sig );
signatureAppearance.setImageScale(javacast("float", .5));
// add image to signature
signatureAppearance.setImage(signature);

I found this post about putting an image in the signature, but i just dont understand their code.

http://www.opensubscriber.com/message/itext-questions@lists.sourceforge.net/6923859.html

I dont understand these lines mostly:
sap.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
sap.setRender(PdfSignatureAppearance.SignatureRenderGraphicAndDescription);

I cant find a setRender method anywhere in signatureAppearance.

Any clues or help would be appreciated!

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep