feat: updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"project": {
|
||||
"name": "sign-pdf",
|
||||
"main": "SampleMain",
|
||||
"main": "me.hatter.tool.signpdf.main.SignPdfMain",
|
||||
"archiveName": "sign-pdf"
|
||||
},
|
||||
"application": false,
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.io.OutputStream;
|
||||
* @author Thomas Chojecki
|
||||
*/
|
||||
class CMSProcessableInputStream implements CMSTypedData {
|
||||
private InputStream in;
|
||||
private final InputStream in;
|
||||
private final ASN1ObjectIdentifier contentType;
|
||||
|
||||
CMSProcessableInputStream(InputStream is) {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package me.hatter.tool.signpdf;
|
||||
|
||||
import me.hatter.tools.commons.security.cert.X509CertUtil;
|
||||
import me.hatter.tools.commons.security.key.KeyUtil;
|
||||
import me.hatter.tool.signpdf.options.SignOptions;
|
||||
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.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
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
|
||||
@@ -29,52 +29,14 @@ import java.util.List;
|
||||
* @author John Hewson
|
||||
*/
|
||||
public class CreateSignature extends CreateSignatureBase {
|
||||
private final SignOptions signOptions;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
public CreateSignature(SignOptions signOptions, KeyStore keystore, char[] pin)
|
||||
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, CertificateException, IOException {
|
||||
super(keystore, pin);
|
||||
this.signOptions = signOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
@@ -84,28 +46,27 @@ public class CreateSignature extends CreateSignatureBase {
|
||||
|
||||
// sign
|
||||
try (
|
||||
FileOutputStream fos = new FileOutputStream(outFile);
|
||||
PDDocument doc = PDDocument.load(inFile)
|
||||
final FileOutputStream fos = new FileOutputStream(outFile);
|
||||
final PDDocument doc = PDDocument.load(inFile)
|
||||
) {
|
||||
signDetached(doc, fos);
|
||||
}
|
||||
}
|
||||
|
||||
public void signDetached(PDDocument document, OutputStream output)
|
||||
throws IOException {
|
||||
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");
|
||||
throw new IllegalStateException(
|
||||
"No changes to the document are permitted due to DocMDP transform parameters dictionary");
|
||||
}
|
||||
|
||||
// create signature dictionary
|
||||
PDSignature signature = new PDSignature();
|
||||
final 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?
|
||||
signature.setName(signOptions.getName());
|
||||
signature.setLocation(signOptions.getLocation());
|
||||
signature.setReason(signOptions.getReason());
|
||||
|
||||
// the signing date, needed for valid signature
|
||||
signature.setSignDate(Calendar.getInstance());
|
||||
@@ -117,13 +78,13 @@ public class CreateSignature extends CreateSignatureBase {
|
||||
|
||||
if (isExternalSigning()) {
|
||||
document.addSignature(signature);
|
||||
ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
|
||||
final ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
|
||||
// invoke external signature service
|
||||
byte[] cmsSignature = sign(externalSigning.getContent());
|
||||
final byte[] cmsSignature = sign(externalSigning.getContent());
|
||||
// set signature bytes received from the service
|
||||
externalSigning.setSignature(cmsSignature);
|
||||
} else {
|
||||
SignatureOptions signatureOptions = new SignatureOptions();
|
||||
final 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
|
||||
@@ -133,163 +94,4 @@ public class CreateSignature extends CreateSignatureBase {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -28,30 +28,18 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
||||
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();
|
||||
final 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);
|
||||
final Certificate[] certChain = keystore.getCertificateChain(alias);
|
||||
if (certChain == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -87,36 +75,21 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
||||
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];
|
||||
final CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
final X509Certificate cert = (X509Certificate) certificateChain[0];
|
||||
// TODO use customer signer
|
||||
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
|
||||
final 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);
|
||||
final CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
|
||||
CMSSignedData signedData = gen.generate(msg, false);
|
||||
if (tsaUrl != null && tsaUrl.length() > 0) {
|
||||
ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
|
||||
final ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
|
||||
signedData = validation.addSignedTimeStamp(signedData);
|
||||
}
|
||||
return signedData.getEncoded();
|
||||
|
||||
@@ -79,8 +79,6 @@ public class SigUtils {
|
||||
* 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) {
|
||||
@@ -117,9 +115,6 @@ public class SigUtils {
|
||||
/**
|
||||
* 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 {
|
||||
@@ -149,9 +144,6 @@ public class SigUtils {
|
||||
|
||||
/**
|
||||
* Log if the certificate is not valid for timestamping.
|
||||
*
|
||||
* @param x509Certificate
|
||||
* @throws CertificateParsingException
|
||||
*/
|
||||
public static void checkTimeStampCertificateUsage(X509Certificate x509Certificate)
|
||||
throws CertificateParsingException {
|
||||
@@ -165,9 +157,6 @@ public class SigUtils {
|
||||
|
||||
/**
|
||||
* Log if the certificate is not valid for responding.
|
||||
*
|
||||
* @param x509Certificate
|
||||
* @throws CertificateParsingException
|
||||
*/
|
||||
public static void checkResponderCertificateUsage(X509Certificate x509Certificate)
|
||||
throws CertificateParsingException {
|
||||
@@ -181,10 +170,6 @@ public class SigUtils {
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package me.hatter.tool.signpdf;
|
||||
|
||||
public class SignPdfMain {
|
||||
}
|
||||
@@ -34,12 +34,6 @@ public class TSAClient {
|
||||
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;
|
||||
@@ -47,30 +41,24 @@ public class TSAClient {
|
||||
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);
|
||||
final byte[] hash = digest.digest(messageImprint);
|
||||
|
||||
// 32-bit cryptographic nonce
|
||||
SecureRandom random = new SecureRandom();
|
||||
int nonce = random.nextInt();
|
||||
final SecureRandom random = new SecureRandom();
|
||||
final int nonce = random.nextInt();
|
||||
|
||||
// generate TSA request
|
||||
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
|
||||
final TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
|
||||
tsaGenerator.setCertReq(true);
|
||||
ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
|
||||
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
|
||||
final ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
|
||||
final TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
|
||||
|
||||
// get TSA response
|
||||
byte[] tsaResponse = getTSAResponse(request.getEncoded());
|
||||
final byte[] tsaResponse = getTSAResponse(request.getEncoded());
|
||||
|
||||
TimeStampResponse response;
|
||||
final TimeStampResponse response;
|
||||
try {
|
||||
response = new TimeStampResponse(tsaResponse);
|
||||
response.validate(request);
|
||||
@@ -78,7 +66,7 @@ public class TSAClient {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
TimeStampToken token = response.getTimeStampToken();
|
||||
final TimeStampToken token = response.getTimeStampToken();
|
||||
if (token == null) {
|
||||
throw new IOException("Response does not have a time stamp token");
|
||||
}
|
||||
|
||||
@@ -32,24 +32,15 @@ import java.util.List;
|
||||
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");
|
||||
final 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));
|
||||
@@ -57,15 +48,10 @@ public class ValidationTimeStamp {
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
public CMSSignedData addSignedTimeStamp(CMSSignedData signedData) throws IOException {
|
||||
final SignerInformationStore signerStore = signedData.getSignerInfos();
|
||||
final List<SignerInformation> newSigners = new ArrayList<>();
|
||||
|
||||
for (SignerInformation signer : signerStore.getSigners()) {
|
||||
// This adds a timestamp to every signer (into his unsigned attributes) in the signature.
|
||||
@@ -79,27 +65,22 @@ public class ValidationTimeStamp {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
private SignerInformation signTimeStamp(SignerInformation signer) throws IOException {
|
||||
final 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,
|
||||
final byte[] token = tsaClient.getTimeStampToken(signer.getSignature());
|
||||
final ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
|
||||
final ASN1Encodable signatureTimeStamp = new Attribute(oid,
|
||||
new DERSet(ASN1Primitive.fromByteArray(token)));
|
||||
|
||||
vector.add(signatureTimeStamp);
|
||||
Attributes signedAttributes = new Attributes(vector);
|
||||
final 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,
|
||||
|
||||
58
src/main/java/me/hatter/tool/signpdf/main/SignPdfMain.java
Normal file
58
src/main/java/me/hatter/tool/signpdf/main/SignPdfMain.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package me.hatter.tool.signpdf.main;
|
||||
|
||||
import me.hatter.tool.signpdf.CreateSignature;
|
||||
import me.hatter.tool.signpdf.options.SignOptions;
|
||||
import me.hatter.tools.commons.security.cert.X509CertUtil;
|
||||
import me.hatter.tools.commons.security.key.KeyUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
public class SignPdfMain {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
final SignOptions signOptions = new SignOptions();
|
||||
signOptions.setName("Example User");
|
||||
signOptions.setLocation("Hangzhou, ZJ");
|
||||
signOptions.setReason("Testing");
|
||||
final String tsaUrl = "https://hatter.ink/ca/sign_timestamp.action";
|
||||
|
||||
final List<X509Certificate> certs = X509CertUtil.parseX509CertificateList(
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"I6Syqg==\n" +
|
||||
"-----END CERTIFICATE-----\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"O1XW8Bs24gI=\n" +
|
||||
"-----END CERTIFICATE-----\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"UhQrqeqObukNbcGgWhwy3+PysJFky6peB6IexebbVYk=\n" +
|
||||
"-----END CERTIFICATE-----");
|
||||
final PrivateKey privateKey = KeyUtil.parsePrivateKeyPEM("-----BEGIN PRIVATE KEY-----\n" +
|
||||
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCVlt5k68ngIZ3T\n" +
|
||||
"xSGBfudXkCooM8/cjEefv/cI\n" +
|
||||
"-----END PRIVATE KEY-----");
|
||||
|
||||
final X509Certificate[] chain = new X509Certificate[]{certs.get(0)};
|
||||
final 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);
|
||||
|
||||
final CreateSignature signing = new CreateSignature(signOptions, keyStore, new char[0]);
|
||||
// TODO signing.setExternalSigning(true);
|
||||
|
||||
File inFile = new File("/Users/hatterjiang/07-057r7_Web_Map_Tile_Service_Standard.pdf");
|
||||
final String name = inFile.getName();
|
||||
final String substring = name.substring(0, name.lastIndexOf('.'));
|
||||
|
||||
final File outFile = new File(inFile.getParent(), substring + "_signed.pdf");
|
||||
signing.signDetached(inFile, outFile, tsaUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package me.hatter.tool.signpdf.options;
|
||||
|
||||
public class SignOptions {
|
||||
private String name;
|
||||
private String location;
|
||||
private String reason;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user