diff --git a/.gitignore b/.gitignore index 5760a7a..94b7950 100644 --- a/.gitignore +++ b/.gitignore @@ -1,54 +1,10 @@ -# ---> Java -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -# ---> macOS -# General +build +classes .DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - +.gradle +.classpath +.project +.settings +*.iml +*.ipr +*.iws \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..abcb4ab --- /dev/null +++ b/build.gradle @@ -0,0 +1,92 @@ +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'idea' + +def JsonSlurper = Class.forName('groovy.json.JsonSlurper'); +def buildJSON = JsonSlurper.newInstance().parseText(new File("build.json").text) + +if (buildJSON.application) { apply plugin: 'application' } + +def baseProjectName = buildJSON?.project?.name ?: '__project_name__'; +def shellCommandName = baseProjectName +def eclipseProjectName = baseProjectName +def eclipseProjectComment = buildJSON?.project?.comment ?: '__project_name_comment__' +def jarManifestMainClass = buildJSON?.project?.main ?: 'SampleMain' + +if (buildJSON.application) { mainClassName = jarManifestMainClass } +archivesBaseName = buildJSON?.project?.archiveName ?: baseProjectName +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +def addRepo = new File(System.getProperty("user.home"), ".build_add.repo") + +repositories { + mavenLocal() + // mavenCentral() + maven() { url 'https://maven.aliyun.com/repository/central' } + if (addRepo.exists()) { maven() { url addRepo.text.trim() } } +} + +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// '-x test' skip unit test +defaultTasks 'build' + +buildscript { + repositories { + mavenLocal() + maven() { url 'https://maven.aliyun.com/repository/central' } + mavenCentral() + jcenter() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE") + } +} +apply plugin: 'org.springframework.boot' +springBoot { + mainClass = jarManifestMainClass +} + +dependencies { + compile files(fileTree(dir: 'lib', includes: ['*.jar'], excludes: ['*-sources.jar', '*-javadoc.jar'])) + + if (buildJSON.repo != null && buildJSON.repo.dependencies != null) { + buildJSON.repo.dependencies.each { + compile("${it}") + } + } + if (buildJSON.repo != null && buildJSON.repo.testDependencies != null) { + buildJSON.repo.testDependencies.each { + testCompile("${it}") + } + } +} + +eclipse { + project { + name = eclipseProjectName + comment = eclipseProjectComment + } + classpath { + defaultOutputDir = file('classes') + downloadSources = true + file { + whenMerged { classpath -> + classpath.entries.findAll { it.kind=='lib' }.each { + if ((it.path != null) && (it.sourcePath == null) && file(it.path.replace(".jar", "-sources.jar")).exists()) { + it.sourcePath = getFileReferenceFactory().fromPath(it.path.replace(".jar", "-sources.jar")) + } + } + } + } + } +} + +eclipseJdt << { + File f = file('.settings/org.eclipse.core.resources.prefs') + f.write('eclipse.preferences.version=1\n') + f.append('encoding/=utf-8') +} diff --git a/build.json b/build.json new file mode 100644 index 0000000..b8691e4 --- /dev/null +++ b/build.json @@ -0,0 +1,29 @@ +{ + "project": { + "name": "sign-pdf", + "main": "SampleMain", + "archiveName": "sign-pdf" + }, + "application": false, + "java": "1.8", + "builder": { + "name": "gradle", + "version": "3.1" + }, + "repo": { + "dependencies": [ + "me.hatter:commons:3.0", + "org.bouncycastle:bcprov-ext-jdk15on:1.70", + "org.bouncycastle:bcpg-jdk15on:1.70", + "org.bouncycastle:bctls-jdk15on:1.70", + "org.bouncycastle:bcpkix-jdk15on:1.70", + "org.bouncycastle:bcmail-jdk15on:1.70", + "org.bouncycastle:bcprov-jdk15on:1.70", + "org.apache.pdfbox:pdfbox:2.0.16", + "org.apache.pdfbox:jbig2-imageio:3.0.0" + ], + "testDependencies": [ + "junit:junit:4.12" + ] + } +} diff --git a/src/main/java/me/hatter/tool/signpdf/CMSProcessableInputStream.java b/src/main/java/me/hatter/tool/signpdf/CMSProcessableInputStream.java new file mode 100644 index 0000000..33f58a9 --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/CMSProcessableInputStream.java @@ -0,0 +1,52 @@ +package me.hatter.tool.signpdf; + +import org.apache.pdfbox.io.IOUtils; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSTypedData; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +// https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/ +// java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java?view=co + +/** + * Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving + * alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray} + * class. + * + * @author Thomas Chojecki + */ +class CMSProcessableInputStream implements CMSTypedData { + private InputStream in; + private final ASN1ObjectIdentifier contentType; + + CMSProcessableInputStream(InputStream is) { + this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is); + } + + CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is) { + contentType = type; + in = is; + } + + @Override + public Object getContent() { + return in; + } + + @Override + public void write(OutputStream out) throws IOException, CMSException { + // read the content only one time + IOUtils.copy(in, out); + in.close(); + } + + @Override + public ASN1ObjectIdentifier getContentType() { + return contentType; + } +} \ No newline at end of file diff --git a/src/main/java/me/hatter/tool/signpdf/CreateSignature.java b/src/main/java/me/hatter/tool/signpdf/CreateSignature.java new file mode 100644 index 0000000..f93e61c --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/CreateSignature.java @@ -0,0 +1,295 @@ +package me.hatter.tool.signpdf; + +import me.hatter.tools.commons.security.cert.X509CertUtil; +import me.hatter.tools.commons.security.key.KeyUtil; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; + +import java.io.*; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.List; + +// https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/ +// java/org/apache/pdfbox/examples/signature/CreateSignature.java?view=co + +/** + * An example for signing a PDF with bouncy castle. + * A keystore can be created with the java keytool, for example: + *

+ * {@code keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365 + * -v -keyalg RSA -keystore keystore.p12 } + * + * @author Thomas Chojecki + * @author Vakhtang Koroghlishvili + * @author John Hewson + */ +public class CreateSignature extends CreateSignatureBase { + + /** + * Initialize the signature creator with a keystore and certficate password. + * + * @param keystore the pkcs12 keystore containing the signing certificate + * @param pin the password for recovering the key + * @throws KeyStoreException if the keystore has not been initialized (loaded) + * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found + * @throws UnrecoverableKeyException if the given password is wrong + * @throws CertificateException if the certificate is not valid as signing time + * @throws IOException if no certificate could be found + */ + public CreateSignature(KeyStore keystore, char[] pin) + throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, CertificateException, IOException { + super(keystore, pin); + } + + /** + * Signs the given PDF file. Alters the original file on disk. + * + * @param file the PDF file to sign + * @throws IOException if the file could not be read or written + */ + public void signDetached(File file) throws IOException { + signDetached(file, file, null); + } + + /** + * Signs the given PDF file. + * + * @param inFile input PDF file + * @param outFile output PDF file + * @throws IOException if the input file could not be read + */ + public void signDetached(File inFile, File outFile) throws IOException { + signDetached(inFile, outFile, null); + } + + /** + * Signs the given PDF file. + * + * @param inFile input PDF file + * @param outFile output PDF file + * @param tsaUrl optional TSA url + * @throws IOException if the input file could not be read + */ + public void signDetached(File inFile, File outFile, String tsaUrl) throws IOException { + if (inFile == null || !inFile.exists()) { + throw new FileNotFoundException("Document for signing does not exist"); + } + + setTsaUrl(tsaUrl); + + // sign + try ( + FileOutputStream fos = new FileOutputStream(outFile); + PDDocument doc = PDDocument.load(inFile) + ) { + signDetached(doc, fos); + } + } + + public void signDetached(PDDocument document, OutputStream output) + throws IOException { + int accessPermissions = SigUtils.getMDPPermission(document); + if (accessPermissions == 1) { + throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary"); + } + + // create signature dictionary + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName("Example User"); + signature.setLocation("Hangzhou, ZJ"); + signature.setReason("Testing"); + // TODO extract the above details from the signing certificate? Reason as a parameter? + + // the signing date, needed for valid signature + signature.setSignDate(Calendar.getInstance()); + + // Optional: certify + if (accessPermissions == 0) { + SigUtils.setMDPPermission(document, signature, 2); + } + + if (isExternalSigning()) { + document.addSignature(signature); + ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output); + // invoke external signature service + byte[] cmsSignature = sign(externalSigning.getContent()); + // set signature bytes received from the service + externalSigning.setSignature(cmsSignature); + } else { + SignatureOptions signatureOptions = new SignatureOptions(); + // Size can vary, but should be enough for purpose. + signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2); + // register signature dictionary and sign interface + document.addSignature(signature, this, signatureOptions); + + // write incremental (only for signing purpose) + document.saveIncremental(output); + } + } + + public static void main(String[] args) throws Exception { + List certs = X509CertUtil.parseX509CertificateList( + "-----BEGIN CERTIFICATE-----\n" + + "MIID8DCCAligAwIBAgIVAO+jr2FNZq/QefgFLTU/+MTMmFiHMA0GCSqGSIb3DQEB\n" + + "CwUAMCQxIjAgBgNVBAMMGVRlc3QgT25seSBJbnRlcm1lZGlhdGUgQ0EwHhcNMTkw\n" + + "OTAzMDAwMDAwWhcNMjQwOTAzMDAwMDAwWjAVMRMwEQYDVQQDDApoYXR0ZXIuaW5r\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlZbeZOvJ4CGd06Rg3ego\n" + + "tLp1dgqPQSZb7E8zPga6EGXpMKuigfyGyLIlLBi345mq8/BYov+8G3LaDJj++7yP\n" + + "O+FcUrFDGABP6CHZCgmYzGHA41V8pV/qXeq0AeIQ9OaczdigydVqoY9S/rjuMLTU\n" + + "B1Y9MMIlhwZoXuheF++qLZJlqbWAwM9UICgtgRoM+mBTZeaM43D2JeuOxmSqZfQJ\n" + + "DqgirHcSCsClKkxhL9p5Vpaa/Sh/5iR3uCNr60N3dQZmv8fUyZPoZbHDZFoELEjy\n" + + "CJSknF6ISQ6x0EzoT4hHGRbbdZGSNgpJiLMlmkEGDKYRk0V+xkFkKtiPWfWk1hiz\n" + + "cQIDAQABo4GnMIGkMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1Ud\n" + + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAlBgNVHREEHjAcggpoYXR0ZXIuaW5r\n" + + "gg53d3cuaGF0dGVyLmluazAdBgNVHQ4EFgQU83K5sj8Al7wIk1qLInjkwfTJRkQw\n" + + "HwYDVR0jBBgwFoAUDx5JXiPyUvc3E6Hkhk5fwgASM9wwDQYJKoZIhvcNAQELBQAD\n" + + "ggGBAEsM17TNw+4YxwjWRxSWHIgakyHpyNrSgG87g0iO671nCnzSEC5UsgtvwVOl\n" + + "IRgOi6iZWdY6UAa0Fp8qtsbmeUchf1RZiD1phkHPCNQ90UY7zBfr+7BJvoSyYLUc\n" + + "85GEdHR9aT2zeIzbYtf1TLeUaxYhGWRFxr+BxWUaenKUUx1MKmE7qYA+NgkLcS0b\n" + + "Emnaulzrt4Jmoa2GJ3WUpvekAiUdaLtLjLe2cTL4cbRgAZu1dS0l/rRpJVnsSR4s\n" + + "3cUIx+cQamfarzTD85PRe+2tlUlE5rNjSCCY9Ev9fRuqMU8x5gr9d1U7/cENpIkv\n" + + "/MxNJtTv3P1e9IL4Nd/84jalN4gRxHvWfREfKMxICjKrhaH4uIkpz2kNwapxu2Rf\n" + + "eL/CtcW9efT1Sgp8pMwV/jv0loKRhGqWDpiRd9JgX+2VZBJuPqvyYxGKo9BHorT0\n" + + "vu6K4y1vb0c0euyrRE+kh4GfuPPWFRWYB9nug7zwxIMdV/o1kA/RrHV6zlA3QBlk\n" + + "I6Syqg==\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIEtDCCApygAwIBAgIUE3K6bToXpBc0QsO/9BWpE4oqNzswDQYJKoZIhvcNAQEL\n" + + "BQAwHDEaMBgGA1UEAwwRVGVzdCBPbmx5IFJvb3QgQ0EwHhcNMTkwODAyMDAwMDAw\n" + + "WhcNMzkwODAyMDAwMDAwWjAkMSIwIAYDVQQDDBlUZXN0IE9ubHkgSW50ZXJtZWRp\n" + + "YXRlIENBMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwoKoXjE/m4pi\n" + + "T6DB5l0J/yRwChUVt1aJ6/qB2dnGSwKSQSOEMMi5/d+JFRinfytc5lMxPiE/A/g2\n" + + "4a9y8+2zyCtWY3UL8xVvJoylhoURmYM2Nx/odMFHoeHESeJkSMDVUN6zZOjGf3eg\n" + + "RYEKjaGIGSlctn8WOejNRbgMxxe4DrKhyAgHaAgiUX9vqX0IHnO5+8Gk260FEa1d\n" + + "CVZ0HInM2/awIjrsTzu3ivXoZ68YijN6JqX0Fu+VSBJhtj6gZrUqJQQIfZja+Z7n\n" + + "OFiV/n4MOOGZdV0FzKdxfk/DKym/7sEdKeZ3TtCwq1M+OohKG5EZcOeMlUXa5Aco\n" + + "bZhVlNxDGZjQ323t17BPSiix2Kr6Y2nJ8B3BO4258nn5K5F0vEYqVs5G4tqONA5T\n" + + "sm54CwGcRQbGXQQkb7Xgn9yg8v42HLQekov6cbnCyIxQqOmkNZzVLcdkqAeSSS6m\n" + + "oGIE+/74GWPn4k+5Wm5bxLqmkeg9MnjXezjDjzBCWJDVQt/GbWHfAgMBAAGjZjBk\n" + + "MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQP\n" + + "HkleI/JS9zcToeSGTl/CABIz3DAfBgNVHSMEGDAWgBSNezwcll5FV1zbEHyVFQVE\n" + + "G/Oa/DANBgkqhkiG9w0BAQsFAAOCAgEAGOXsHuJppjLLMUrWmcPd3fGmXgGsf0+Z\n" + + "v60OG39fNfnyxD2UDOmoLRgAixaWGc3Mj6gRRoPK1w2yjazbLhcIL9Cdh+F23cSq\n" + + "dK3RVR+TJk+/YVC+voxxKPXTilMNmoy4yOXKG88U9xwNdSYpwwgjjwhVwMZCpFn4\n" + + "rVc79f4UQ0tlRlSO55LO7RDx/LLSdm0wVAWWZJ/U9aWr2qglwS6Qi3aLDIbOn3l7\n" + + "7JzkLVDOHp3mnpxaOjpK4Ge0OiTIvoeIfEwhf25He2dYsJgrqKtvlkqEZ7J/b2mg\n" + + "mGwi7j7chu7PRNzzOjvzogZApyq8qVk1vn5fcyzMX2qxfhHWQWvWGqIWK2d5fNeg\n" + + "NTGIcxSZfKNBI8iiJ/+Wsp/fGoGaOfx63yiQ+6GsI/02sqz4qJrhsZcUuV+NnpWv\n" + + "e/LZgzSCkw92He0BwkNkAqb9HY5n+VSPuscOHD1xoaj0tovzlvktG4M3kyScqd2o\n" + + "nCO8yD+CZG3xNGIjYH1IKZ3NVE1SaW2RlkzJuzkcAgzpduRaEYPGM3qTIuks6GBR\n" + + "dfuaZ9FjI+c2pQIKUMOcvmYQru286sWXaLFRYF3ZLrNNjdBs0Oawk2YJ9rp4dmMY\n" + + "xVZByucYuEu/b4Y/CzLB8N6cn87lYT+YkUBWPMLkN4nUbykUBckrNvB1DqGNOjXA\n" + + "O1XW8Bs24gI=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "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\n" + + "-----END CERTIFICATE-----"); + PrivateKey privateKey = KeyUtil.parsePrivateKeyPEM("-----BEGIN PRIVATE KEY-----\n" + + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCVlt5k68ngIZ3T\n" + + "xSGBfudXkCooM8/cjEefv/cI\n" + + "-----END PRIVATE KEY-----"); + + X509Certificate[] chain = new X509Certificate[]{certs.get(0)}; + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + for (X509Certificate c : certs) { + keyStore.setCertificateEntry(c.getSubjectX500Principal().getName(), c); + } + keyStore.setKeyEntry("", privateKey, new char[0], chain); + + System.out.println(keyStore); + + CreateSignature signing = new CreateSignature(keyStore, new char[0]); +// signing.setExternalSigning(true); + + String tsaUrl = "https://hatter.ink/ca/sign_timestamp.action"; + File inFile = new File("/Users/hatterjiang/07-057r7_Web_Map_Tile_Service_Standard.pdf"); + String name = inFile.getName(); + String substring = name.substring(0, name.lastIndexOf('.')); + + File outFile = new File(inFile.getParent(), substring + "_signed.pdf"); + signing.signDetached(inFile, outFile, tsaUrl); + } + + public static void main_old(String[] args) throws IOException, GeneralSecurityException { + if (args.length < 3) { + usage(); + System.exit(1); + } + + String tsaUrl = null; + boolean externalSig = false; + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-tsa")) { + i++; + if (i >= args.length) { + usage(); + System.exit(1); + } + tsaUrl = args[i]; + } + if (args[i].equals("-e")) { + externalSig = true; + } + } + + // load the keystore + KeyStore keystore = KeyStore.getInstance("PKCS12"); + char[] password = args[1].toCharArray(); // TODO use Java 6 java.io.Console.readPassword + keystore.load(new FileInputStream(args[0]), password); + // TODO alias command line argument + + // sign PDF + CreateSignature signing = new CreateSignature(keystore, password); + signing.setExternalSigning(externalSig); + + File inFile = new File(args[2]); + String name = inFile.getName(); + String substring = name.substring(0, name.lastIndexOf('.')); + + File outFile = new File(inFile.getParent(), substring + "_signed.pdf"); + signing.signDetached(inFile, outFile, tsaUrl); + } + + private static void usage() { + System.err.println("usage: java " + CreateSignature.class.getName() + " " + + " \n" + "" + + "options:\n" + + " -tsa sign timestamp using the given TSA server\n" + + " -e sign using external signature creation scenario"); + } +} \ No newline at end of file diff --git a/src/main/java/me/hatter/tool/signpdf/CreateSignatureBase.java b/src/main/java/me/hatter/tool/signpdf/CreateSignatureBase.java new file mode 100644 index 0000000..f46d0ad --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/CreateSignatureBase.java @@ -0,0 +1,144 @@ +package me.hatter.tool.signpdf; + +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Enumeration; + +// https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/ +// org/apache/pdfbox/examples/signature/CreateSignatureBase.java?view=co +public abstract class CreateSignatureBase implements SignatureInterface { + private PrivateKey privateKey; + private Certificate[] certificateChain; + private String tsaUrl; + private boolean externalSigning; + + /** + * Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the + * signature. + * + * @param keystore is a pkcs12 keystore. + * @param pin is the pin for the keystore / private key + * @throws KeyStoreException if the keystore has not been initialized (loaded) + * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found + * @throws UnrecoverableKeyException if the given password is wrong + * @throws CertificateException if the certificate is not valid as signing time + * @throws IOException if no certificate could be found + */ + public CreateSignatureBase(KeyStore keystore, char[] pin) + throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException { + // grabs the first alias from the keystore and get the private key. An + // alternative method or constructor could be used for setting a specific + // alias that should be used. + Enumeration aliases = keystore.aliases(); + String alias; + Certificate cert = null; + while (aliases.hasMoreElements()) { + alias = aliases.nextElement(); + setPrivateKey((PrivateKey) keystore.getKey(alias, pin)); + Certificate[] certChain = keystore.getCertificateChain(alias); + if (certChain == null) { + continue; + } + setCertificateChain(certChain); + cert = certChain[0]; + if (cert instanceof X509Certificate) { + // avoid expired certificate + ((X509Certificate) cert).checkValidity(); + + SigUtils.checkCertificateUsage((X509Certificate) cert); + } + break; + } + + if (cert == null) { + throw new IOException("Could not find certificate"); + } + } + + public final void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + } + + public final void setCertificateChain(final Certificate[] certificateChain) { + this.certificateChain = certificateChain; + } + + public Certificate[] getCertificateChain() { + return certificateChain; + } + + public void setTsaUrl(String tsaUrl) { + this.tsaUrl = tsaUrl; + } + + /** + * SignatureInterface sample implementation. + *

+ * This method will be called from inside of the pdfbox and create the PKCS #7 signature. + * The given InputStream contains the bytes that are given by the byte range. + *

+ * This method is for internal use only. + *

+ * Use your favorite cryptographic library to implement PKCS #7 signature creation. + * If you want to create the hash and the signature separately (e.g. to transfer only the hash + * to an external application), read this + * answer or this answer. + * + * @throws IOException + */ + @Override + public byte[] sign(InputStream content) throws IOException { + // cannot be done private (interface) + try { + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + X509Certificate cert = (X509Certificate) certificateChain[0]; + // TODO use customer signer + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey); + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build()).build(contentSigner, cert)); + gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain))); + CMSProcessableInputStream msg = new CMSProcessableInputStream(content); + CMSSignedData signedData = gen.generate(msg, false); + if (tsaUrl != null && tsaUrl.length() > 0) { + ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl); + signedData = validation.addSignedTimeStamp(signedData); + } + return signedData.getEncoded(); + } catch (GeneralSecurityException | CMSException | OperatorCreationException e) { + throw new IOException(e); + } + } + + /** + * Set if external signing scenario should be used. + * If {@code false}, SignatureInterface would be used for signing. + *

+ * Default: {@code false} + *

+ * + * @param externalSigning {@code true} if external signing should be performed + */ + public void setExternalSigning(boolean externalSigning) { + this.externalSigning = externalSigning; + } + + public boolean isExternalSigning() { + return externalSigning; + } +} \ No newline at end of file diff --git a/src/main/java/me/hatter/tool/signpdf/SigUtils.java b/src/main/java/me/hatter/tool/signpdf/SigUtils.java new file mode 100644 index 0000000..45d587f --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/SigUtils.java @@ -0,0 +1,204 @@ +package me.hatter.tool.signpdf; + +import me.hatter.tools.commons.log.LogTool; +import me.hatter.tools.commons.log.LogTools; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.asn1.x509.KeyPurposeId; + +import java.io.IOException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +// https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/ +// org/apache/pdfbox/examples/signature/SigUtils.java?view=co + +/** + * Utility class for the signature / timestamp examples. + * + * @author Tilman Hausherr + */ +public class SigUtils { + private static final LogTool LOG = LogTools.getLogTool(SigUtils.class); + + private SigUtils() { + } + + /** + * Get the access permissions granted for this document in the DocMDP transform parameters + * dictionary. Details are described in the table "Entries in the DocMDP transform parameters + * dictionary" in the PDF specification. + * + * @param doc document. + * @return the permission value. 0 means no DocMDP transform parameters dictionary exists. Other + * return values are 1, 2 or 3. 2 is also returned if the DocMDP transform parameters dictionary + * is found but did not contain a /P entry, or if the value is outside the valid range. + */ + public static int getMDPPermission(PDDocument doc) { + COSBase base = doc.getDocumentCatalog().getCOSObject().getDictionaryObject(COSName.PERMS); + if (base instanceof COSDictionary) { + COSDictionary permsDict = (COSDictionary) base; + base = permsDict.getDictionaryObject(COSName.DOCMDP); + if (base instanceof COSDictionary) { + COSDictionary signatureDict = (COSDictionary) base; + base = signatureDict.getDictionaryObject("Reference"); + if (base instanceof COSArray) { + COSArray refArray = (COSArray) base; + for (int i = 0; i < refArray.size(); ++i) { + base = refArray.getObject(i); + if (base instanceof COSDictionary) { + COSDictionary sigRefDict = (COSDictionary) base; + if (COSName.DOCMDP.equals(sigRefDict.getDictionaryObject("TransformMethod"))) { + base = sigRefDict.getDictionaryObject("TransformParams"); + if (base instanceof COSDictionary) { + COSDictionary transformDict = (COSDictionary) base; + int accessPermissions = transformDict.getInt(COSName.P, 2); + if (accessPermissions < 1 || accessPermissions > 3) { + accessPermissions = 2; + } + return accessPermissions; + } + } + } + } + } + } + } + return 0; + } + + /** + * Set the access permissions granted for this document in the DocMDP transform parameters + * dictionary. Details are described in the table "Entries in the DocMDP transform parameters + * dictionary" in the PDF specification. + * + * @param doc The document. + * @param signature The signature object. + * @param accessPermissions The permission value (1, 2 or 3). + */ + static public void setMDPPermission(PDDocument doc, PDSignature signature, int accessPermissions) { + COSDictionary sigDict = signature.getCOSObject(); + + // DocMDP specific stuff + COSDictionary transformParameters = new COSDictionary(); + transformParameters.setItem(COSName.TYPE, COSName.getPDFName("TransformParams")); + transformParameters.setInt(COSName.P, accessPermissions); + transformParameters.setName(COSName.V, "1.2"); + transformParameters.setNeedToBeUpdated(true); + + COSDictionary referenceDict = new COSDictionary(); + referenceDict.setItem(COSName.TYPE, COSName.getPDFName("SigRef")); + referenceDict.setItem("TransformMethod", COSName.DOCMDP); + referenceDict.setItem("DigestMethod", COSName.getPDFName("SHA1")); + referenceDict.setItem("TransformParams", transformParameters); + referenceDict.setNeedToBeUpdated(true); + + COSArray referenceArray = new COSArray(); + referenceArray.add(referenceDict); + sigDict.setItem("Reference", referenceArray); + referenceArray.setNeedToBeUpdated(true); + + // Catalog + COSDictionary catalogDict = doc.getDocumentCatalog().getCOSObject(); + COSDictionary permsDict = new COSDictionary(); + catalogDict.setItem(COSName.PERMS, permsDict); + permsDict.setItem(COSName.DOCMDP, signature); + catalogDict.setNeedToBeUpdated(true); + permsDict.setNeedToBeUpdated(true); + } + + /** + * Log if the certificate is not valid for signature usage. Doing this + * anyway results in Adobe Reader failing to validate the PDF. + * + * @param x509Certificate + * @throws CertificateParsingException + */ + public static void checkCertificateUsage(X509Certificate x509Certificate) + throws CertificateParsingException { + // Check whether signer certificate is "valid for usage" + // https://stackoverflow.com/a/52765021/535646 + // https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html#id1 + boolean[] keyUsage = x509Certificate.getKeyUsage(); + if (keyUsage != null && !keyUsage[0] && !keyUsage[1]) { + // (unclear what "signTransaction" is) + // https://tools.ietf.org/html/rfc5280#section-4.2.1.3 + LOG.error("Certificate key usage does not include " + + "digitalSignature nor nonRepudiation"); + } + List extendedKeyUsage = x509Certificate.getExtendedKeyUsage(); + if (extendedKeyUsage != null && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_emailProtection.toString()) && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_codeSigning.toString()) && + !extendedKeyUsage.contains(KeyPurposeId.anyExtendedKeyUsage.toString()) && + !extendedKeyUsage.contains("1.2.840.113583.1.1.5") && + // not mentioned in Adobe document, but tolerated in practice + !extendedKeyUsage.contains("1.3.6.1.4.1.311.10.3.12")) { + LOG.error("Certificate extended key usage does not include " + + "emailProtection, nor codeSigning, nor anyExtendedKeyUsage, " + + "nor 'Adobe Authentic Documents Trust'"); + } + } + + /** + * Log if the certificate is not valid for timestamping. + * + * @param x509Certificate + * @throws CertificateParsingException + */ + public static void checkTimeStampCertificateUsage(X509Certificate x509Certificate) + throws CertificateParsingException { + List extendedKeyUsage = x509Certificate.getExtendedKeyUsage(); + // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 + if (extendedKeyUsage != null && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_timeStamping.toString())) { + LOG.error("Certificate extended key usage does not include timeStamping"); + } + } + + /** + * Log if the certificate is not valid for responding. + * + * @param x509Certificate + * @throws CertificateParsingException + */ + public static void checkResponderCertificateUsage(X509Certificate x509Certificate) + throws CertificateParsingException { + List extendedKeyUsage = x509Certificate.getExtendedKeyUsage(); + // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 + if (extendedKeyUsage != null && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_OCSPSigning.toString())) { + LOG.error("Certificate extended key usage does not include OCSP responding"); + } + } + + /** + * Gets the last relevant signature in the document, i.e. the one with the highest offset. + * + * @param document to get its last signature + * @return last signature or null when none found + * @throws IOException + */ + public static PDSignature getLastRelevantSignature(PDDocument document) throws IOException { + SortedMap sortedMap = new TreeMap<>(); + for (PDSignature signature : document.getSignatureDictionaries()) { + int sigOffset = signature.getByteRange()[1]; + sortedMap.put(sigOffset, signature); + } + if (sortedMap.size() > 0) { + PDSignature lastSignature = sortedMap.get(sortedMap.lastKey()); + COSBase type = lastSignature.getCOSObject().getItem(COSName.TYPE); + if (type.equals(COSName.SIG) || type.equals(COSName.DOC_TIME_STAMP)) { + return lastSignature; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/me/hatter/tool/signpdf/SignPdfMain.java b/src/main/java/me/hatter/tool/signpdf/SignPdfMain.java new file mode 100644 index 0000000..2d403b7 --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/SignPdfMain.java @@ -0,0 +1,4 @@ +package me.hatter.tool.signpdf; + +public class SignPdfMain { +} diff --git a/src/main/java/me/hatter/tool/signpdf/TSAClient.java b/src/main/java/me/hatter/tool/signpdf/TSAClient.java new file mode 100644 index 0000000..5124fae --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/TSAClient.java @@ -0,0 +1,142 @@ +package me.hatter.tool.signpdf; + +import me.hatter.tools.commons.io.RStream; +import me.hatter.tools.commons.log.LogTool; +import me.hatter.tools.commons.log.LogTools; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.tsp.*; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.URL; +import java.net.URLConnection; +import java.security.MessageDigest; +import java.security.SecureRandom; + +// https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/ +// org/apache/pdfbox/examples/signature/TSAClient.java?revision=1792644&view=co + +/** + * Time Stamping Authority (TSA) Client [RFC 3161]. + * + * @author Vakhtang Koroghlishvili + * @author John Hewson + */ +public class TSAClient { + private static final LogTool LOG = LogTools.getLogTool(TSAClient.class); + + private final URL url; + private final String username; + private final String password; + private final MessageDigest digest; + + /** + * @param url the URL of the TSA service + * @param username user name of TSA + * @param password password of TSA + * @param digest the message digest to use + */ + public TSAClient(URL url, String username, String password, MessageDigest digest) { + this.url = url; + this.username = username; + this.password = password; + this.digest = digest; + } + + /** + * @param messageImprint imprint of message contents + * @return the encoded time stamp token + * @throws IOException if there was an error with the connection or data from the TSA server, + * or if the time stamp response could not be validated + */ + public byte[] getTimeStampToken(byte[] messageImprint) throws IOException { + digest.reset(); + byte[] hash = digest.digest(messageImprint); + + // 32-bit cryptographic nonce + SecureRandom random = new SecureRandom(); + int nonce = random.nextInt(); + + // generate TSA request + TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator(); + tsaGenerator.setCertReq(true); + ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm()); + TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce)); + + // get TSA response + byte[] tsaResponse = getTSAResponse(request.getEncoded()); + + TimeStampResponse response; + try { + response = new TimeStampResponse(tsaResponse); + response.validate(request); + } catch (TSPException e) { + throw new IOException(e); + } + + TimeStampToken token = response.getTimeStampToken(); + if (token == null) { + throw new IOException("Response does not have a time stamp token"); + } + + return token.getEncoded(); + } + + // gets response data for the given encoded TimeStampRequest data + // throws IOException if a connection to the TSA cannot be established + private byte[] getTSAResponse(byte[] request) throws IOException { + LOG.debug("Opening connection to TSA server"); + + final URLConnection connection = url.openConnection(); + connection.setConnectTimeout(3000); + connection.setReadTimeout(10000); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setRequestProperty("Content-Type", "application/timestamp-query"); + + LOG.debug("Established connection to TSA server"); + + if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) { + connection.setRequestProperty(username, password); + } + + // read response + try (OutputStream output = connection.getOutputStream()) { + output.write(request); + } + + LOG.debug("Waiting for response from TSA server"); + + byte[] response = RStream.from(connection.getInputStream()).bytesAndClose(); + + LOG.debug("Received response from TSA server"); + + return response; + } + + // returns the ASN.1 OID of the given hash algorithm + private ASN1ObjectIdentifier getHashObjectIdentifier(String algorithm) { + switch (algorithm) { + case "MD2": + return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md2.getId()); + case "MD5": + return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md5.getId()); + case "SHA-1": + return new ASN1ObjectIdentifier(OIWObjectIdentifiers.idSHA1.getId()); + case "SHA-224": + return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha224.getId()); + case "SHA-256": + return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId()); + case "SHA-384": + return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha384.getId()); + case "SHA-512": + return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha512.getId()); + default: + return new ASN1ObjectIdentifier(algorithm); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/hatter/tool/signpdf/ValidationTimeStamp.java b/src/main/java/me/hatter/tool/signpdf/ValidationTimeStamp.java new file mode 100644 index 0000000..ca73a10 --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/ValidationTimeStamp.java @@ -0,0 +1,109 @@ +package me.hatter.tool.signpdf; + +import org.apache.pdfbox.io.IOUtils; +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.Attributes; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +// https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/ +// org/apache/pdfbox/examples/signature/ValidationTimeStamp.java?view=co + +/** + * This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed + * TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp) + * + * @author Others + * @author Alexis Suter + */ +public class ValidationTimeStamp { + private TSAClient tsaClient; + + /** + * @param tsaUrl The url where TS-Request will be done. + * @throws NoSuchAlgorithmException + * @throws MalformedURLException + */ + public ValidationTimeStamp(String tsaUrl) throws NoSuchAlgorithmException, MalformedURLException { + if (tsaUrl != null) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + this.tsaClient = new TSAClient(new URL(tsaUrl), null, null, digest); + } + } + + /** + * Creates a signed timestamp token by the given input stream. + * + * @param content InputStream of the content to sign + * @return the byte[] of the timestamp token + * @throws IOException + */ + public byte[] getTimeStampToken(InputStream content) throws IOException { + return tsaClient.getTimeStampToken(IOUtils.toByteArray(content)); + } + + /** + * Extend cms signed data with TimeStamp first or to all signers + * + * @param signedData Generated CMS signed data + * @return CMSSignedData Extended CMS signed data + * @throws IOException + */ + public CMSSignedData addSignedTimeStamp(CMSSignedData signedData) + throws IOException { + SignerInformationStore signerStore = signedData.getSignerInfos(); + List newSigners = new ArrayList<>(); + + for (SignerInformation signer : signerStore.getSigners()) { + // This adds a timestamp to every signer (into his unsigned attributes) in the signature. + newSigners.add(signTimeStamp(signer)); + } + + // Because new SignerInformation is created, new SignerInfoStore has to be created + // and also be replaced in signedData. Which creates a new signedData object. + return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners)); + } + + /** + * Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes. + * + * @param signer information about signer + * @return information about SignerInformation + * @throws IOException + */ + private SignerInformation signTimeStamp(SignerInformation signer) + throws IOException { + AttributeTable unsignedAttributes = signer.getUnsignedAttributes(); + + ASN1EncodableVector vector = new ASN1EncodableVector(); + if (unsignedAttributes != null) { + vector = unsignedAttributes.toASN1EncodableVector(); + } + + byte[] token = tsaClient.getTimeStampToken(signer.getSignature()); + ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken; + ASN1Encodable signatureTimeStamp = new Attribute(oid, + new DERSet(ASN1Primitive.fromByteArray(token))); + + vector.add(signatureTimeStamp); + Attributes signedAttributes = new Attributes(vector); + + // There is no other way changing the unsigned attributes of the signer information. + // result is never null, new SignerInformation always returned, + // see source code of replaceUnsignedAttributes + return SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(signedAttributes)); + } +} \ No newline at end of file