feat: init commit

This commit is contained in:
2023-10-29 12:41:03 +08:00
parent f67f991769
commit 29a6e32379
10 changed files with 1080 additions and 53 deletions

62
.gitignore vendored
View File

@@ -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

92
build.gradle Normal file
View File

@@ -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/<project>=utf-8')
}

29
build.json Normal file
View File

@@ -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"
]
}
}

View File

@@ -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;
}
}

View File

@@ -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:
* <p>
* {@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<X509Certificate> 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" +
"MIIFLDCCAxSgAwIBAgIVAKBORHv9Qd/+nqn1WtCh46Suwq11MA0GCSqGSIb3DQEB\n" +
"CwUAMBwxGjAYBgNVBAMMEVRlc3QgT25seSBSb290IENBMCAXDTE5MDgwMjAwMDAw\n" +
"MFoYDzIwNzkwODAyMDAwMDAwWjAcMRowGAYDVQQDDBFUZXN0IE9ubHkgUm9vdCBD\n" +
"QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALaKEyt8PnET4vzqXcOO\n" +
"yVn2pY5tqg14wHVdtnEu7DGxzHO3lKCLxaOuNQkCJ0hRDutC+O2niqAzZ9uvgFpu\n" +
"wemEaciRVjT+IXgSuFSdqGtnGHeHTm0Pwxpbsg4d9KCMDCRoeSUHDf5/j1ZU79iv\n" +
"CNpidRVYqJQMhrgS/nJM8us/lZ4BIoAeHOyMUBIR3GhdFjgwQxVDPVModNOu8rBV\n" +
"MuJQLXZLbERh1PqAGrJ9yJ8+O8qB+xSf/nJtW7fyRSMSLFNu9sOUUMxaDer/uAkj\n" +
"r6SZN2tszABY25rW3nxChL25s2Hbe2psEmhCCaKVBroEsKWQkvK33+eiy9OhuAWM\n" +
"1KVBztcwX4go0vGFEVu9NbJEnqlPL9Mlwy6tMbwmsktSEn1RHYAwXXdtgmwRWo8S\n" +
"Bykhxoc8ED9aKMtZkdjVa34YLTVqD03WFM68j7dSLw/4ikc8MLsSnjLcXaLwGzHV\n" +
"/C/7pkz0N+VQnp65Ju09XpneJ3H6iYuQHyzO5V949vL8b4EpinOWQBGmw9Zhr1Z5\n" +
"MAv33AW5BP+GzHNLzDJU8Z0vn8mZR7SMzreWqHZ8I1eipvKuibmf9kwApVZthlYr\n" +
"94zUScO9o451zeofJhENLoGK0n8jy8m4uz0+XCd3kxplSmrknWHQBmm3SXLfWZmV\n" +
"ve9zwsPh2sBhB39jH6buQwR1AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" +
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBSNezwcll5FV1zbEHyVFQVEG/Oa/DAfBgNV\n" +
"HSMEGDAWgBSNezwcll5FV1zbEHyVFQVEG/Oa/DANBgkqhkiG9w0BAQsFAAOCAgEA\n" +
"U4KMc1cde06QUULlDWg8+vCYc/Ve76zpfHdPSfoh2u9ZXa2ZTCCriX3byUDV8CAL\n" +
"FUBYajtHDSEJS/JZLz9RDPieGz2O7p7u5p/lVis79dUaLChp/FsMuhDMYlyOIZfi\n" +
"sQhRoLqpH2MAuBn9FjyciDPP3d698iOQdj/8pztuFh3GUdTmjkgeTve4i52oxRi9\n" +
"o9GuakE6+rHQi/5jjWtutrqDcnnmzK4mYup9T2eqYoPeWe3kBmALpfoKoabjXDwq\n" +
"61EreV7qfO+0HmcFNhqv5gOJBoa7tQkCMnn4EmJMzEa3tZqXXc3Pd5W9L+Q7gD+p\n" +
"4Hi+exrr6KLl9A6Kht+HKoB3em8E7USD5uU3PJZEsSeYhSKCTeroArKc19S6vmU0\n" +
"PIHWeaZWbwySQrDw1FbLiItw5ZCXGFQLmDDcW4gWw2EP43ZfcJl068nrcOHDHsTU\n" +
"pDP8TzmT/uhp1HRbh4vuyOej2qPVIzUEa0fwyg9JyAuD7+hoH07qZ2Ekc4Ocij6w\n" +
"Hm88RHyD6ObPRg/aZd7fVkgQgz/cE1drH3gO5yUvDQhB3Iz3SFaHwWEfYi3tiPLz\n" +
"wA3rEgaSj6xhZTeOkKVu+aj3u19tcyIRxTXM33Zv1WLSRVUo/CJj7O38qBft4F+L\n" +
"UhQrqeqObukNbcGgWhwy3+PysJFky6peB6IexebbVYk=\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() + " " +
"<pkcs12_keystore> <password> <pdf_to_sign>\n" + "" +
"options:\n" +
" -tsa <url> sign timestamp using the given TSA server\n" +
" -e sign using external signature creation scenario");
}
}

View File

@@ -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<String> 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.
* <p>
* 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.
* <p>
* This method is for internal use only.
* <p>
* 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 <a href="https://stackoverflow.com/questions/41767351">this
* answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
*
* @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.
* <p>
* Default: {@code false}
* </p>
*
* @param externalSigning {@code true} if external signing should be performed
*/
public void setExternalSigning(boolean externalSigning) {
this.externalSigning = externalSigning;
}
public boolean isExternalSigning() {
return externalSigning;
}
}

View File

@@ -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<String> 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<String> 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<String> 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<Integer, PDSignature> 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;
}
}

View File

@@ -0,0 +1,4 @@
package me.hatter.tool.signpdf;
public class SignPdfMain {
}

View File

@@ -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);
}
}
}

View File

@@ -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<SignerInformation> 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));
}
}