Skip to content

Commit

Permalink
Merge pull request #26 from KernelPanic80/master
Browse files Browse the repository at this point in the history
Add reason/location fields of signature and support for PKCS11
  • Loading branch information
jmarxuach authored Aug 2, 2023
2 parents 0a0cf58 + 739a8d2 commit ba66e46
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 45 deletions.
54 changes: 43 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
![Java CI with Maven](https://github.com/jmarxuach/BatchPDFSign/workflows/Java%20CI%20with%20Maven/badge.svg)

Command line tool for digital signature of PDF files, useful for example in Batch processes, from non Java programming languages like Php, Shell scripts, etc...

BatchPDFSign is a command line to sign PDF file with a PKCS12 certificate.

To use it you need:
- a PKCS12 certificate. It should be a <filename>.pfx or <filename>.p12 file.
BatchPDFSign is a command line to sign PDF file with a PKCS12 certificate or with a PKCS11 Hardware Token.

To use it you need:

- a PKCS12 certificate. It should be a `<filename>`.pfx or `<filename>`.p12 file.
- a password for the certificate
- and a PDF file to sign.

For PKCS11 version you need:

- a PKCS11 config file. `name=MyToken\nlibrary=/path/to/lib.so\n`
- a PIN/password for the certificate
- and a PDF file to sign.

## self signed certificate creation

You can create your own self signed certificate with this following 4 commands in Ubuntu.

```bash
Expand All @@ -22,13 +30,16 @@ openssl pkcs12 -export -out myself.pfx -inkey myself.key -in myself.crt
```

## Signing

### Synopsis

```
usage: BatchPDFSignPortable
-m,--mode <arg> pkcs12 or pkcs11 (defaults to pkcs12)
-i,--input <arg> input file path
-k,--key <arg> key file path
-k,--key <arg> key file path or pkcs11 config file
-o,--output <arg> output file
-p,--password <arg> keyfile password
-p,--password <arg> keyfile password or PIN
--page <arg> page of signature rectangle; needs to be specified
to output signature rectangle
--fs <arg> font size of text in signature rectangle (default:
Expand All @@ -43,36 +54,57 @@ usage: BatchPDFSignPortable
be specified as well
--signtext <arg> signature text; needs --page to be specified as
--tsa <arg> URI of the time service authority (TSA)
--reason <arg> Reason field of signature
--location <arg> Location field of signature
```

### Mode

PKCS mode operation `pkcs12` or `pkcs11`. Defaults to `pkcs12`.

### key file path

This parameter is the certificate you want to sign the pdf with. It can be generated with the code documented in the chapter self signed certificate creation.

For `pkcs11` mode, this file is the config file of the HSM/Token ex: `name=MyToken\nlibrary=/path/to/lib.so\n`

### password

This parameter is the password for the certificate. The password is set during the creation of the certificate file.

For `pkcs11` mode, this is the PIN/Password for the certificate.

### input file path

The file you want to sign.

### output file

If this parameter is set, a new file with this name will be created and signed. The original file will remain untouched.

### visible signature

By default, the signature will not be (easily) visible in the final PDF file. If you want to make it easier for users to see, and with that and some GUIs easier to check the signature, you have to specify the location and size of the "signature rectangle". You also have the option to change the font size and to specify your own text.

- --page
this option is required if you want the signature to appear. If you give this option, you will also have to specify --rx, --ry, --rw and --rh.
this option is required if you want the signature to appear. If you give this option, you will also have to specify --rx, --ry, --rw and --rh.
- --rx and --ry
specify the location of the signature rectangle
specify the location of the signature rectangle
- --rw and --rh
specify the width and height of the signature rectangle
specify the width and height of the signature rectangle
- --fs
specify the font size of the text within the signature rectangle; 12 by default
specify the font size of the text within the signature rectangle; 12 by default
- --signtext
override the standard text provided by the signature-library with your own, provided text
override the standard text provided by the signature-library with your own, provided text
- --reason
Reason field of the signature
- --location
Location field of the signature

## Development

You'll need:

- Maven
- Java 11 JDK

Expand Down
20 changes: 13 additions & 7 deletions lib/src/main/java/BatchPDFSign/lib/BatchPDFSign.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ public BatchPDFSign(String pkcs12FileName, String PkcsPassword, String pdfInputF
* @throws IOException A File couldn't be opened.
* @throws GeneralSecurityException Some permissions aren't right.
*/
public void signFile(int page, float rx, float ry, float rw, float rh, float fs, String signtext) throws IOException, GeneralSecurityException {
signFile(page, rx, ry, rw, rh, fs, signtext, null);
public void signFile(int page, float rx, float ry, float rw, float rh, float fs, String signtext, String reason, String location) throws IOException, GeneralSecurityException {
signFile(page, rx, ry, rw, rh, fs, signtext, null, reason, location);
}
public void signFile(int page, float rx, float ry, float rw, float rh, float fs, String signtext, String tsaUri) throws IOException, GeneralSecurityException {
public void signFile(int page, float rx, float ry, float rw, float rh, float fs, String signtext, String tsaUri, String reason, String location) throws IOException, GeneralSecurityException {
if(tsaUri == null) {
tsaUri = defaultTsaUri;
}
Expand All @@ -77,15 +77,21 @@ public void signFile(int page, float rx, float ry, float rw, float rh, float fs,
ITSAClient tsaClient = new TSAClientBouncyCastle(tsaUri);
StampingProperties properties = new StampingProperties().preserveEncryption();
PdfSigner signer = new PdfSigner(reader, new FileOutputStream(pdfOutputFileName), properties);
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
if (page > 0) {
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
appearance.setPageNumber(page);
appearance.setPageRect(new Rectangle(rx, ry, rw, rh));
appearance.setLayer2FontSize(fs);
if (signtext != null) {
appearance.setLayer2Text(signtext);
}
}
if(reason != null){
appearance.setReason(reason);
}
if(location != null){
appearance.setLocation(location);
}
IExternalDigest digest = new BouncyCastleDigest();
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
Expand All @@ -102,11 +108,11 @@ public void signFile(int page, float rx, float ry, float rw, float rh, float fs,
}
}
}
public void signFile(String tsaUri) throws IOException, GeneralSecurityException {
this.signFile(0, 0, 0, 0, 0, 10, "", tsaUri);
public void signFile(String tsaUri, String reason, String location) throws IOException, GeneralSecurityException {
this.signFile(0, 0, 0, 0, 0, 10, "", tsaUri, reason, location);
}
public void signFile() throws IOException, GeneralSecurityException {
this.signFile(null);
this.signFile(null, null, null);
}

/**
Expand Down
179 changes: 179 additions & 0 deletions lib/src/main/java/BatchPDFSign/lib/BatchPDFSignSmartCard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package BatchPDFSign.lib;

import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.signatures.*;
import com.itextpdf.kernel.geom.Rectangle;
import org.apache.commons.io.FileUtils;

import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Provider;
import java.security.Security;


/**
* Signs PDF files and retains PDF/A conformity.
* @author KernelPanic80, github.com/KernelPanic80
* @author Pep Marxuach, jmarxuach
* @author Joe Meier, Jocomol, [email protected]
* @version 1.0.5.2
*/
public class BatchPDFSignSmartCard {

private static final String SUN_PKCS11_CLASSNAME = "sun.security.pkcs11.SunPKCS11";
private static final String SUN_PKCS11_PROVIDER_NAME = "SunPKCS11";

private static PrivateKey privateKey;
private static Certificate[] certificateChain;
private static String defaultTsaUri = "https://freetsa.org/tsr";

private final String pkcs11ConfFileName;
private final String PkcsPassword;
private final String pdfInputFileName;
private final String pdfOutputFileName;
private final boolean flgRename;
private final File inputFile;

/**
* This is the constructor
* Sets all the class variables.
* If pdfOutputFileName is null, the pdfOutputFileName class variable is set to pdfInputFileName + "-sig"
* @author KernelPanic80, github.com/KernelPanic80
* @author Joe Meier, Jocomol, [email protected]
* @param pkcs11ConfFileName File name of PKCS11 conf file.
* @param PkcsPassword Password of the key (PIN).
* @param pdfInputFileName File name of the PDF file which should be signed.
* @param pdfOutputFileName File name which the signed PDF should have.
*/
public BatchPDFSignSmartCard(String pkcs11ConfFileName, String PkcsPassword, String pdfInputFileName, String pdfOutputFileName){
this.pkcs11ConfFileName = pkcs11ConfFileName;
this.PkcsPassword = PkcsPassword;
this.pdfInputFileName = pdfInputFileName;
this.inputFile = new File(pdfInputFileName);
this.flgRename = pdfOutputFileName == null;
if (! this.flgRename){
this.pdfOutputFileName = pdfOutputFileName;
} else {
this.pdfOutputFileName = pdfInputFileName + "-sig";
}
}

/**
* Signs a PDF file. This method is configured by the constructors of this class.
* @author KernelPanic80, github.com/KernelPanic80
* @author Pep Marxuach, jmarxuach
* @author Joe Meier, Jocomol, [email protected]
* @throws IOException A File couldn't be opened.
* @throws GeneralSecurityException Some permissions aren't right.
*/
public void signFile(int page, float rx, float ry, float rw, float rh, float fs, String signtext) throws IOException, GeneralSecurityException {
signFile(page, rx, ry, rw, rh, fs, signtext, null, null,null);
}
public void signFile(int page, float rx, float ry, float rw, float rh, float fs, String signtext, String tsaUri, String reason, String location) throws IOException, GeneralSecurityException {
if(tsaUri == null) {
tsaUri = defaultTsaUri;
}
// Check PDF input file
if (!inputFile.exists() || inputFile.isDirectory()) {
throw new FileNotFoundException("File: " + this.inputFile + " wasn't found");
}
Provider provider = readPrivateKeyFromPKCS11(pkcs11ConfFileName, PkcsPassword);
PdfReader reader = new PdfReader(pdfInputFileName);
ITSAClient tsaClient = new TSAClientBouncyCastle(tsaUri);
StampingProperties properties = new StampingProperties().preserveEncryption();
PdfSigner signer = new PdfSigner(reader, new FileOutputStream(pdfOutputFileName), properties);
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
if (page > 0) {
appearance.setPageNumber(page);
appearance.setPageRect(new Rectangle(rx, ry, rw, rh));
appearance.setLayer2FontSize(fs);
if (signtext != null) {
appearance.setLayer2Text(signtext);
}
}
if(reason != null){
appearance.setReason(reason);
}
if(location != null){
appearance.setLocation(location);
}
IExternalDigest digest = new BouncyCastleDigest();
IExternalSignature signature = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName());
signer.signDetached(digest, signature, certificateChain, null, null, tsaClient, 0, PdfSigner.CryptoStandard.CADES);
// Renaming signed PDF file
if (flgRename) {
if (this.inputFile.delete())
{
FileUtils.moveFile(FileUtils.getFile(pdfOutputFileName), FileUtils.getFile(pdfInputFileName));
} else {
throw new IOException("File: " + this.inputFile + " couldn't be deleted");
}
}
}
public void signFile(String tsaUri, String reason, String location) throws IOException, GeneralSecurityException {
this.signFile(0, 0, 0, 0, 0, 10, "", tsaUri, reason, location);
}
public void signFile() throws IOException, GeneralSecurityException {
this.signFile(null,null,null);
}

/**
* Instanciates and return a PKCS11 provider based on the config file, and loads the signer key from the smart card.
*
* @author KernelPanic80, github.com/KernelPanic80
* @author Pep Marxuach, jmarxuach
* @author Joe Meier, Jocomol, [email protected]
* @param pkcs11FileName File name of PKCS11 conf file.
* @param pkcs11Password Password/PIN to unlock the key file.
*/
protected static Provider readPrivateKeyFromPKCS11 (String pkcs11ConfFileName, String pkcs11Password) throws KeyStoreException, IOException, UnrecoverableKeyException, NoSuchAlgorithmException, CertificateException {
Provider providerPKCS11;
try {
int javaVersion = BatchPDFSignSmartCard.getVersion();
if (javaVersion >= 9) {
Provider prototype = Security.getProvider(SUN_PKCS11_PROVIDER_NAME);
Class<?> sunPkcs11ProviderClass = Class.forName(SUN_PKCS11_CLASSNAME);
Method configureMethod = sunPkcs11ProviderClass.getMethod("configure", String.class);
providerPKCS11 = (Provider) configureMethod.invoke(prototype, pkcs11ConfFileName);
} else {
Class<?> sunPkcs11ProviderClass = Class.forName(SUN_PKCS11_CLASSNAME);
Constructor<?> constructor = sunPkcs11ProviderClass.getConstructor(String.class);
providerPKCS11 = (Provider) constructor.newInstance(pkcs11ConfFileName);
}
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException ex) {
// Handle any error, log message or throw an exception
ex.printStackTrace(System.out);
throw new KeyStoreException(ex);
}
Security.addProvider(providerPKCS11);
KeyStore ks = KeyStore.getInstance("PKCS11");
ks.load(null, pkcs11Password.toCharArray());
String alias = ks.aliases().nextElement();
privateKey = (PrivateKey) ks.getKey(alias, pkcs11Password.toCharArray());
certificateChain = ks.getCertificateChain(alias);
return providerPKCS11;
}

/**
* Get current Java Version
* @return actual java version
*/
public static int getVersion() {
String version = System.getProperty("java.version");
if(version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf(".");
if(dot != -1) { version = version.substring(0, dot); }
} return Integer.parseInt(version);
}

}
4 changes: 4 additions & 0 deletions portable/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
<manifest>
<mainClass>BatchPDFSign.portable.Main</mainClass>
</manifest>
<manifestEntries>
<Add-Exports>jdk.crypto.cryptoki/sun.security.pkcs11 jdk.crypto.cryptoki/sun.security.pkcs11.wrapper java.base/sun.security.action java.base/sun.security.rsa</Add-Exports>
<Add-Opens>java.base/java.security java.base/sun.security.util</Add-Opens>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
Expand Down
Loading

0 comments on commit ba66e46

Please sign in to comment.