feat: init commit
This commit is contained in:
62
.gitignore
vendored
62
.gitignore
vendored
@@ -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
92
build.gradle
Normal 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
29
build.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
295
src/main/java/me/hatter/tool/signpdf/CreateSignature.java
Normal file
295
src/main/java/me/hatter/tool/signpdf/CreateSignature.java
Normal 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" +
|
||||
"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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() + " " +
|
||||
"<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");
|
||||
}
|
||||
}
|
||||
144
src/main/java/me/hatter/tool/signpdf/CreateSignatureBase.java
Normal file
144
src/main/java/me/hatter/tool/signpdf/CreateSignatureBase.java
Normal 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;
|
||||
}
|
||||
}
|
||||
204
src/main/java/me/hatter/tool/signpdf/SigUtils.java
Normal file
204
src/main/java/me/hatter/tool/signpdf/SigUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
4
src/main/java/me/hatter/tool/signpdf/SignPdfMain.java
Normal file
4
src/main/java/me/hatter/tool/signpdf/SignPdfMain.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package me.hatter.tool.signpdf;
|
||||
|
||||
public class SignPdfMain {
|
||||
}
|
||||
142
src/main/java/me/hatter/tool/signpdf/TSAClient.java
Normal file
142
src/main/java/me/hatter/tool/signpdf/TSAClient.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/main/java/me/hatter/tool/signpdf/ValidationTimeStamp.java
Normal file
109
src/main/java/me/hatter/tool/signpdf/ValidationTimeStamp.java
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user