refer my question about prevent adding another approval/signing after lock Add lock Dictionary on Certify Signature PDFBox
i have succeed to certify then lock, but after lock when i try to signing/adding another signature/approval is still valid, what i expected is after lock if another sign the document becomes invalid
rev 1 -> rev 2 -> certified -> lock -> then rev 4 (come and still valid signature)
this what i'm try:
//Create empty signature field
PDDocument docEx=PDDocument.load(inputFile);
PDAcroForm acroFormField = docEx.getDocumentCatalog().getAcroForm();
acroFormField.getCOSObject().setNeedToBeUpdated(true);
COSObject fields = acroFormField.getCOSObject().getCOSObject(COSName.FIELDS);
if (fields != null)
fields.setNeedToBeUpdated(true);
acroFormField.setSignaturesExist(true);
acroFormField.setAppendOnly(true);
acroFormField.getCOSObject().setDirect(true);
PDPage pageField = docEx.getPage(page);
// Create empty signature field, it will get the name "Signature1"
PDSignatureField signatureField = new PDSignatureField(acroFormField);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDRectangle rectField = new PDRectangle((float)humanRect.getX(), (float)humanRect.getY(), (float)humanRect.getWidth(), (float)humanRect.getHeight());
widget.setRectangle(rectField);
widget.getCOSObject().setNeedToBeUpdated(true);
widget.setPage(pageField);
pageField.getAnnotations().add(widget);
pageField.getCOSObject().setNeedToBeUpdated(true);
acroFormField.getFields().add(signatureField);
setLock(signatureField, acroFormField);
docEx.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true);
docEx.saveIncremental(fos);
fos.close();
//Close and replace with created empty field signature
PDDocument doc = PDDocument.load(signedFile);
FileOutputStream fosSeal = new FileOutputStream(signedFile);
PDSignature signatureLock = new PDSignature();
PDSignatureField signatureFieldLoad = (PDSignatureField) doc.getDocumentCatalog().getAcroForm().getField("Signature1");
LogSystem.info("signatureFieldLoad " + signatureFieldLoad.getValueAsString());
LogSystem.info("signaturelock " + signatureLock);
signatureFieldLoad.setValue(signatureLock);
COSBase lock = signatureFieldLoad.getCOSObject().getDictionaryObject(COS_NAME_LOCK);
if (lock instanceof COSDictionary)
{
COSDictionary lockDict = new COSDictionary();
lockDict.setItem(COS_NAME_ACTION, COS_NAME_ALL);
lockDict.setItem(COSName.TYPE, COS_NAME_SIG_FIELD_LOCK);
COSDictionary transformParams = new COSDictionary(lockDict);
transformParams.setItem(COSName.TYPE, COSName.getPDFName("TransformParams"));
transformParams.setItem(COSName.V, COSName.getPDFName("1.2"));
transformParams.setInt(COSName.P, 1);
transformParams.setDirect(true);
transformParams.setNeedToBeUpdated(true);
COSDictionary sigRef = new COSDictionary();
sigRef.setItem(COSName.TYPE, COSName.getPDFName("SigRef"));
sigRef.setItem(COSName.getPDFName("TransformParams"), transformParams);
sigRef.setItem(COSName.getPDFName("TransformMethod"), COSName.getPDFName("FieldMDP"));
sigRef.setItem(COSName.getPDFName("Data"), doc.getDocumentCatalog());
sigRef.setDirect(true);
COSArray referenceArray = new COSArray();
referenceArray.add(sigRef);
signatureLock.getCOSObject().setItem(COSName.getPDFName("Reference"), referenceArray);
LogSystem.info("LOCK DICTIONARY");
final Predicate<PDField> shallBeLocked;
final COSArray fieldsLock = lockDict.getCOSArray(COSName.FIELDS);
final List<String> fieldNames = fieldsLock == null ? Collections.emptyList() :
fieldsLock.toList().stream().filter(c -> (c instanceof COSString)).map(s -> ((COSString)s).getString()).collect(Collectors.toList());
final COSName action = lockDict.getCOSName(COSName.getPDFName("Action"));
if (action.equals(COSName.getPDFName("Include"))) {
shallBeLocked = f -> fieldNames.contains(f.getFullyQualifiedName());
} else if (action.equals(COSName.getPDFName("Exclude"))) {
shallBeLocked = f -> !fieldNames.contains(f.getFullyQualifiedName());
} else if (action.equals(COSName.getPDFName("All"))) {
shallBeLocked = f -> true;
} else { // unknown action, lock nothing
shallBeLocked = f -> false;
}
lockFields(doc.getDocumentCatalog().getAcroForm().getFields(), shallBeLocked);
setMDPPermission(doc, signatureLock, 2);
// default filter
signatureLock.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
// subfilter for basic and PAdES Part 2 signatures
signatureLock.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signatureLock.setName(name);
// the signing date, needed for valid signature
signatureLock.setSignDate(getDate());
// do not set SignatureInterface instance, if external signing used
SignatureInterface signatureInterface = isExternalSigning() ? null : this;
doc.addSignature(signatureLock, signatureInterface, signatureOptions);
doc.getDocumentCatalog().getAcroForm().getField("Signature1").setPartialName(SignatureField);
if (isExternalSigning()) {
this.tsaUrl=tsaUrl;
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fosSeal);
// invoke external signature serviceUsing fallback
byte[] cmsSignature =IOUtils.toByteArray(externalSigning.getContent());
String sgn=null;
try {
sgn = signingProcess(cmsSignature);
}catch(Exception e)
{
LogSystem.error(e.toString());
e.printStackTrace();
doc.close();
Telegram tele = new Telegram();
tele.Send(e.toString());
return false;
}
// set signature bytes received from the service
if (sgn != null)
{
externalSigning.setSignature(attachSignature(sgn));
}
}
any suggest ? or anyone can tell me how it work if i want to prevent the document changes ? thank you
Related
Working with java use Apache PDFBox to sign and certified, invalid certified if signature exist, with JsignPDF was able to certified when approval exist, the process is sign after that do certified (seal) document
Signature with JsignPDF
document after certified with JsignPDF :
the certified invalid with PDFBox
the function from PDFBox with some editing was to try :
public void signPDF(File inputFile, File signedFile, Rectangle2D humanRect, String tsaUrl, int page, String SignatureField, boolean isWithQR, String name, UserSignature userSignature) throws IOException, CertificateEncodingException, NoSuchAlgorithmException, OperatorCreationException, CMSException {
if (inputFile == null || !inputFile.exists()) {
throw new IOException("Document for signing does not exist");
}
setTsaUrl(tsaUrl);
// creating output document and prepare the IO streams.
// try (FileOutputStream fos = new FileOutputStream(signedFile);
// PDDocument doc = Loader.loadPDF(inputFile)) {
// creating output document and prepare the IO streams.
FileOutputStream fos = new FileOutputStream(signedFile);
// load document
PDDocument doc = PDDocument.load(inputFile);
int accessPermissions = SigUtils.getMDPPermission(doc);
LogSystem.info("Document permission " + accessPermissions);
if (accessPermissions == 1) {
throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
}
// Note that PDFBox has a bug that visual signing on certified files with permission 2
// doesn't work properly, see PDFBOX-3699. As long as this issue is open, you may want to
// be careful with such files.
PDSignature signature = null;
PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm(null);
PDRectangle rect = null;
// sign a PDF with an existing empty signature, as created by the CreateEmptySignatureForm example.
if (acroForm != null) {
signature = findExistingSignature(acroForm, SignatureField);
if (signature != null) {
rect = acroForm.getField(SignatureField).getWidgets().get(0).getRectangle();
}
}
if (signature == null) {
// create signature dictionary
signature = new PDSignature();
}
if (rect == null) {
rect = createSignatureRectangle(doc, humanRect);
}
// Optional: certify
// can be done only if version is at least 1.5 and if not already set
// doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
// PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
// if (doc.getVersion() >= 1.5f && accessPermissions == 0)
// {
// SigUtils.setMDPPermission(doc, signature, 2);
// }
if (acroForm != null && acroForm.getNeedAppearances()) {
// PDFBOX-3738 NeedAppearances true results in visible signature becoming invisible
// with Adobe Reader
if (acroForm.getFields().isEmpty()) {
// we can safely delete it if there are no fields
acroForm.getCOSObject().removeItem(COSName.NEED_APPEARANCES);
// note that if you've set MDP permissions, the removal of this item
// may result in Adobe Reader claiming that the document has been changed.
// and/or that field content won't be displayed properly.
// ==> decide what you prefer and adjust your code accordingly.
} else {
System.out.println("/NeedAppearances is set, signature may be ignored by Adobe Reader");
}
}
// default filter
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
// subfilter for basic and PAdES Part 2 signatures
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("Name");
signature.setLocation("Location");
if(userSignature.getType().equals("sign"))
{
signature.setReason("Reason");
if (accessPermissions == 0) {
COSDictionary sigDict = signature.getCOSObject();
// DocMDP specific stuff
COSDictionary transformParameters = new COSDictionary();
transformParameters.setItem(COSName.TYPE, COSName.TRANSFORM_PARAMS);
transformParameters.setInt(COSName.P, 0);
transformParameters.setNeedToBeUpdated(true);
COSDictionary referenceDict = new COSDictionary();
referenceDict.setItem(COSName.TYPE, COSName.SIG_REF);
referenceDict.setItem(COSName.TRANSFORM_METHOD, COSName.DOCMDP);
referenceDict.setItem(COSName.DIGEST_METHOD, COSName.getPDFName("SHA256"));
referenceDict.setItem(COSName.TRANSFORM_PARAMS, transformParameters);
referenceDict.setNeedToBeUpdated(true);
COSArray referenceArray = new COSArray();
referenceArray.add(referenceDict);
sigDict.setItem(COSName.REFERENCE, referenceArray);
referenceArray.setNeedToBeUpdated(true);
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);
}
}
if(userSignature.getType().equals("seal"))
{
signature.setReason(userSignature.getQrText());
// try {
// if (doc.getVersion() >= 1.5f && accessPermissions == 0)
// {
// SigUtils.setMDPPermission(doc, signature, 2);
// }
// }catch(Exception e)
// {
// e.printStackTrace();
// }
if (accessPermissions == 0) {
// SigUtils.setMDPPermission(document, signature, 1);
COSDictionary sigDict = signature.getCOSObject();
// DocMDP specific stuff
COSDictionary transformParameters = new COSDictionary();
transformParameters.setItem(COSName.TYPE, COSName.TRANSFORM_PARAMS);
transformParameters.setInt(COSName.P, 2);
transformParameters.setNeedToBeUpdated(true);
COSDictionary referenceDict = new COSDictionary();
referenceDict.setItem(COSName.TYPE, COSName.SIG_REF);
referenceDict.setItem(COSName.TRANSFORM_METHOD, COSName.DOCMDP);
referenceDict.setItem(COSName.DIGEST_METHOD, COSName.getPDFName("SHA256"));
referenceDict.setItem(COSName.TRANSFORM_PARAMS, transformParameters);
referenceDict.setNeedToBeUpdated(true);
COSArray referenceArray = new COSArray();
referenceArray.add(referenceDict);
sigDict.setItem(COSName.REFERENCE, referenceArray);
referenceArray.setNeedToBeUpdated(true);
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);
}
}
// the signing date, needed for valid signature
signature.setSignDate(Calendar.getInstance());
// do not set SignatureInterface instance, if external signing used
SignatureInterface signatureInterface = isExternalSigning() ? null : this;
// register signature dictionary and sign interface
signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, 0, rect, signature, isWithQR, name, userSignature.getDescOnly(), userSignature.getType(), userSignature.isVisible()));
signatureOptions.setPage(page);
doc.addSignature(signature, signatureInterface, signatureOptions);
doc.getDocumentCatalog().getAcroForm().getField("Signature1").setPartialName(SignatureField);
if (isExternalSigning()) {
this.tsaUrl=tsaUrl;
// ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
// // invoke external signature service
// byte[] cmsSignature = sign(externalSigning.getContent());
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
// invoke external signature service
byte[] cmsSignature =IOUtils.toByteArray(externalSigning.getContent());
String sgn= signingProcess(cmsSignature);
// set signature bytes received from the service
externalSigning.setSignature(attachSignature(sgn));
} else {
// write incremental (only for signing purpose)
doc.saveIncremental(fos);
}
doc.close();
// } catch (CertificateEncodingException e) {
// e.printStackTrace();
// } catch (NoSuchAlgorithmException e) {
// e.printStackTrace();
// } catch (OperatorCreationException e) {
// e.printStackTrace();
// } catch (CMSException e) {
// e.printStackTrace();
// }
// Do not close signatureOptions before saving, because some COSStream objects within
// are transferred to the signed document.
// Do not allow signatureOptions get out of scope before saving, because then the COSDocument
// in signature options might by closed by gc, which would close COSStream objects prematurely.
// See https://issues.apache.org/jira/browse/PDFBOX-3743
IOUtils.closeQuietly(signatureOptions);
}
set setMDPermission to certified the document :
if (accessPermissions == 0) {
COSDictionary sigDict = signature.getCOSObject();
// DocMDP specific stuff
COSDictionary transformParameters = new COSDictionary();
transformParameters.setItem(COSName.TYPE, COSName.TRANSFORM_PARAMS);
transformParameters.setInt(COSName.P, 1);
transformParameters.setNeedToBeUpdated(true);
COSDictionary referenceDict = new COSDictionary();
referenceDict.setItem(COSName.TYPE, COSName.SIG_REF);
referenceDict.setItem(COSName.TRANSFORM_METHOD, COSName.DOCMDP);
referenceDict.setItem(COSName.DIGEST_METHOD, COSName.getPDFName("SHA256"));
referenceDict.setItem(COSName.TRANSFORM_PARAMS, transformParameters);
referenceDict.setNeedToBeUpdated(true);
COSArray referenceArray = new COSArray();
referenceArray.add(referenceDict);
sigDict.setItem(COSName.REFERENCE, referenceArray);
referenceArray.setNeedToBeUpdated(true);
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);
}
Signature with the parameter "sign" and Certified with parameter "seal"
*Update, on PDFBox the first existing signature always be certified, not second signature ? can the last sign be certified and swap the first one ?
any suggest ?
This answer essentially is a more detailed version of the comments, essentially finding that what you want to do is not possible.
You ask for a way to
Certify Document when Approval Signature exist
According to the current PDF specification:
ISO 32000-2:2020 subsection 12.8.1 "General" of 12.8 "Digital signatures"
A PDF document may contain the following standard types of signatures: [...] One or more approval signatures (also known as recipient signatures). These shall follow the certification signature if one is present.
Thus, you cannot validly add a certification signature to a PDF which already has approval signatures.
(This does not automatically mean that all validators will detect the issue if you add a certification signature after approval signatures, let alone correctly display the cause. Many validators only do a subset of the checks that strictly speaking are necessary...)
More question if the certification signature come first then next is approval like 3 signature approval, can the last the certification setMDPPermission with 1 ? so at the end the document can't add more approval signature
setMDPPermission adds a DocMDP transform to the signature, and such a transform makes the signature a certification signature. Thus, using this method when signing a document that already has an approval signature, will create an invalid PDF or fail entirely.
You can lock a PDF document with an approval signature, though, if you add a Lock dictionary to the signature field with a P entry of 1. Beware, though, this is a ISO 32000-2 feature originally introduced as a feature of an Adobe Extension to ISO 32000-1. Not all PDF viewers support ISO 32000-2 yet, so some viewers may not respect this entry.
I am creating a PADES signature using pdfbox 3.0.0 RC, my code works using the example to create the digital signature. However, I am unable to see the signature level in Adobe Acrobat when I open the document with this tool although it is able to validate my signature.
I am not creating the VRI so I am guessing that this might be an issue but then if this is necessary to validate my signature I don't understand why the signature is displayed as valid?
Adobe Acrobat Signature:
/**
* Service for automatically signing a document as part of a workflow. In this instance no user information is
* gathered
*
* #param taskID
* #param processName which will be added to the document
* #param keyID the ID for the key used to sign the PDF document
* #return the signed PDF document as a base 64 Encoded String
*/
#Transactional
public String signPDFService(String processID,
String processName,
String keyID,
ObjectData signatureImage,
String creator)
{
try {
ByteArrayOutputStream ostream = new ByteArrayOutputStream();
//Optional<ObjectData> pdfDocumentProcessInstance = userDataRepository.findById(documents.get(0).getProcessID());
/*List<Task> tasks = taskService.createTaskQuery()
.taskId(taskID)
.list();
// Optional<ObjectData> pdfDocumentProcessInstance = userDataRepository.findById(documents.get(0).getProcessID());
List<PDFDocument> documents = tasks.stream()
.map(task -> {
String processID = task.getProcessInstanceId();
Map<String, Object> variables = taskService.getVariables(task.getId());
PDFDocument document;
try {
document = new PDFDocument(
(ArrayList) variables.get("assigneeList"),
(String) variables.get("unsignedPDFDocument"),
task.getProcessInstanceId(),
task.getId(),
(String) variables.get("name"),
(String) variables.get("description")
);
document.setHistory((ArrayList) variables.get("history"));
return document;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
} catch (CMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
})
.collect(Collectors.toList());*/
//Optional<ObjectData> pdfDocumentProcessInstance = userDataRepository.findById(documents.get(0).getProcessID());
Optional<ObjectData> pdfDocumentProcessInstance = userDataRepository.findById(processID);
if(pdfDocumentProcessInstance.isEmpty())
throw new IOException("No process found");
String pdfDocumentBase64String = pdfDocumentProcessInstance.get().getAttributes().get("PDFDocument");
String extractedPDFString = pdfDocumentBase64String.replaceAll("data:application/pdf;base64,", "").replaceAll("data:;base64,", "").replaceAll("data:application/octet-stream", "");
//String extractedPDFString = base64PDF.replaceAll("data:application/pdf;base64,", "").replaceAll("data:;base64,", "");
InputStream stream = new ByteArrayInputStream(Base64.getDecoder().decode(extractedPDFString.getBytes()));
//Create the date object to sign the document
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
//Retrieve certificate chain for the PDF Signer
String certChainPEM = kmsService.getCertChainPEM(keyID);
X509Certificate pdfSignerCertificate = X509Utils.readCertificateChain(certChainPEM).get(0).getCertificate();
//Create the CMS Signing Object
ExternalSignatureCMSSignedDataGenerator cmsGenerator = new ExternalSignatureCMSSignedDataGenerator();
ExternalSignatureSignerInfoGenerator signerGenerator = new ExternalSignatureSignerInfoGenerator(CMSSignedDataGenerator.DIGEST_SHA256, "1.2.840.10045.4.3.2");
signerGenerator.setCertificate(pdfSignerCertificate);
ExternalSigningSupport externalSigningSupport;
PDDocument pdDocument = Loader.loadPDF(stream);
//Create the PDFBox Signature Object
PDSignature pdSignature = new PDSignature();
pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
pdSignature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);
pdSignature.setLocation("Remote IS Blocks Signer");
pdSignature.setName("IS Blocks Signer");
pdSignature.setReason(processName);
pdDocument.setDocumentId(calendar.getTimeInMillis());
pdSignature.setSignDate(calendar);
// Optional: Certify the first time signature
// can be done only if version is at least 1.5 and if not already set
// doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
// PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
int accessPermissions = SigUtils.getMDPPermission(pdDocument);
if (pdDocument.getVersion() >= 1.5f && accessPermissions == 0 && processName.contains("Document Certifying Key"))
{
logger.debug("Certifying Document");
SigUtils.setMDPPermission(pdDocument, pdSignature, 3);
}
if(signatureImage != null) {
String data = signatureImage.getAttributes().get("data").replaceAll("data:application/pdf;base64,", "").replaceAll("data:;base64,", "").replaceAll("data:image/png;base64,", "");
int pageNumber = Integer.parseInt(signatureImage.getAttributes().get("page"));
float x = Float.parseFloat(signatureImage.getAttributes().get("x"));
float y = Float.parseFloat(signatureImage.getAttributes().get("y"));
float width = Float.parseFloat(signatureImage.getAttributes().get("width"));
float height = Float.parseFloat(signatureImage.getAttributes().get("height"));
SignatureOptions signatureOptions;
// register signature dictionary and sign interface
signatureOptions = new SignatureOptions();
PDFVisibleSignature pdfVisibleSignature = new PDFVisibleSignature();
signatureOptions.setVisualSignature(pdfVisibleSignature.createVisualSignatureTemplate(
x,
y,
width,
height,
pdDocument,
pageNumber,
pdSignature,
Base64.getDecoder().decode(data.getBytes("UTF-8"))));
signatureOptions.setPage(pageNumber);
pdDocument.addSignature(pdSignature, null, signatureOptions);
} else {
pdDocument.addSignature(pdSignature);
}
externalSigningSupport = pdDocument.saveIncrementalForExternalSigning(ostream);
//Create the message digest of the pre-signed PDF
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = org.apache.commons.io.IOUtils.toByteArray(externalSigningSupport.getContent());
byte[] hashBytes = digest.digest(bytes);
//CMS Signature
InputStream isBytes = new ByteArrayInputStream(bytes);
CMSProcessable input = new CMSProcessableInputStream(isBytes);
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
MessageDigest messageDigest1 = MessageDigest.getInstance("SHA-256");
byte[] hash = messageDigest1.digest(bytes);
byte[] bytesToSign = signerGenerator.getBytesToSign(PKCSObjectIdentifiers.data, hash, new Date(),
"BC");
String encodedData = Base64.getEncoder().encodeToString(bytesToSign);
logger.debug("Bytes to Sign:" + (Base64.getEncoder().encodeToString(bytesToSign)));
logger.debug("Hash:" + Base64.getEncoder().encodeToString(hash));
//Create the signature using the keyID
//At this time only ECDSAWithSHA256 is supported
Map<String, String> signature = kmsService.sign(keyID, encodedData);
byte[] signedBytes = Base64.getDecoder().decode(signature.get("signature"));
X509Certificate[] chain;
signerGenerator.setCertificate(pdfSignerCertificate);
signerGenerator.setSignedBytes(signedBytes);
cmsGenerator.addSignerInf(signerGenerator);
cmsGenerator.addCertificatesAndCRLs(X509Utils.getCertStore(signature.get("certificateChain")));
CMSSignedData signedData = cmsGenerator.generate(new CMSProcessableByteArray(hash), false);
//Add a RFC3161 Time Stamp
ValidationTimeStamp validation = new ValidationTimeStamp("https://freetsa.org/tsr");
signedData = validation.addSignedTimeStamp(signedData);
ContentSigner nonSigner = new ContentSigner() {
#Override
public byte[] getSignature() {
return signedBytes;
}
#Override
public OutputStream getOutputStream() {
return new ByteArrayOutputStream();
}
#Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
return new DefaultSignatureAlgorithmIdentifierFinder().find( "SHA256WithECDSA" );
}
};
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build());
gen.addCertificate(new X509CertificateHolder(pdfSignerCertificate.getEncoded()));
sigb.setDirectSignature( true );
gen.addSignerInfoGenerator(sigb.build(nonSigner, new X509CertificateHolder(pdfSignerCertificate.getEncoded())));
CMSTypedData msg = new CMSProcessableInputStream( new ByteArrayInputStream( "not used".getBytes() ) );
CMSSignedData signedData1 = gen.generate((CMSTypedData)msg, false);
signedData1.getEncoded();
externalSigningSupport.setSignature(signedData.getEncoded());
//documents.get(0).addHistoricEvent("Signed " + processName);
//ArrayList<String> history = documents.get(0).getHistory();
//Post Signature
String signedPDFDocument = Base64.getEncoder().encodeToString(ostream.toByteArray());
PDDocument newPdf1;
newPdf1 = Loader.loadPDF(ostream.toByteArray());
byte[] fileContent = ostream.toByteArray();
List<PDSignature> pdfSignatures;
pdfSignatures = newPdf1.getSignatureDictionaries();
byte[] signatureAsBytes;
signatureAsBytes = newPdf1.getLastSignatureDictionary().getContents( fileContent );
byte[] signedContentAsBytes;
signedContentAsBytes = newPdf1.getLastSignatureDictionary().getSignedContent( fileContent );
// Now we construct a PKCS #7 or CMS.
CMSProcessable cmsProcessableInputStream = new CMSProcessableByteArray(signedContentAsBytes);
CMSSignedData cmsSignedData;
cmsSignedData = new CMSSignedData(cmsProcessableInputStream, signatureAsBytes);
Store certificatesStore = cmsSignedData.getCertificates();
Collection<SignerInformation> signers = cmsSignedData.getSignerInfos().getSigners();
SignerInformation signerInformation = signers.iterator().next();
Collection matches = certificatesStore.getMatches(signerInformation.getSID());
X509CertificateHolder certificateHolder = (X509CertificateHolder) matches.iterator().next();
ObjectMapper mapper = new ObjectMapper();
ArrayList<String> signatures = null;
//signatures = documents.get(0).getSignatures();
for(int iCount = 0; iCount < pdfSignatures.size(); iCount++) {
PDFSignature pdfSignature = new PDFSignature(
pdfSignatures.get(iCount).getName(),
pdfSignatures.get(iCount).getLocation(),
pdfSignatures.get(iCount).getSignDate().getDisplayName(Calendar.LONG_FORMAT, java.util.Calendar.LONG, Locale.UK),
pdfSignatures.get(iCount).getReason(),
certificateHolder.getSubject().toString(),
certificateHolder.getIssuer().toString(),
Base64.getEncoder().encodeToString(certificateHolder.getEncoded()));
//signatures.add(mapper.writeValueAsString(pdfSignature));
logger.info("Signature" + mapper.writeValueAsString(pdfSignature));
}
Map<String, Object> variables = new HashMap<String, Object>();
// variables.put("history", history);
//variables.put("unsignedPDFDocument", signedPDFDocument);
//variables.put("signatures", signatures);
//variables.put("status", value)
Map<String, String> pdfDocumentProcessInstanceAttributes = pdfDocumentProcessInstance.get().getAttributes();
pdfDocumentProcessInstanceAttributes.put("PDFDocument", signedPDFDocument);
ObjectData newpdfProcessInstance = pdfDocumentProcessInstance.get();
newpdfProcessInstance.setAttributes(pdfDocumentProcessInstanceAttributes);
userDataRepository.save(newpdfProcessInstance);
newpdfProcessInstance.getHistory().add(new Date() + "Signed by:" + creator);
System.out.println(newpdfProcessInstance.getId() + " " + newpdfProcessInstance.toString());
newPdf1.close();
ostream.close();
} catch (Exception e){
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
While analyzing the file document-with signingTime.pdf you provided in a comment, I recognized an issue in it. Being aware of that issue I re-checked your original document-17 21.08.14.pdf and also recognized that issue therein, so maybe this issue causes the validation problem you're here to solve. Thus, ...
Both your example files (document-17 21.08.14.pdf and document-with signingTime.pdf) contain each actually two concatenated copies of the same, multi-revision PDF with a single signature Signature1, merely the second copy has a changed ID entry. Added to them are incremental updates with a signature Signature2.
%PDF-1.4
...
15 0 obj
<<
/FT /Sig
...
/T (Signature1)
...
>>
endobj
...
/ID [<1952AB9C134E46B58251246E985D5C15> <7F887303DDC0ED7C37AE77403E30DFB0>]
...
%%EOF
%PDF-1.4
...
15 0 obj
<<
/FT /Sig
...
/T (Signature1)
...
>>
endobj
...
/ID [<1952AB9C134E46B58251246E985D5C15> <A57CD5B87222756EC4A096125C7E8A42>]
...
%%EOF
...
35 0 obj
<<
/FT /Sig
...
/T (Signature2)
...
>>
...
%%EOF
This structure is broken, even though it does not give rise to wrong cross references (as the first copy and the second copy are identical except for the ID, the cross references and the startxref offsets point to the respective positions in the first copy). Adobe Reader signature validation can react quite sensitive to such issues.
Thus, you should remove the second copy here to get ahead.
Furthermore, as already mentioned in a comment the SignerInfo of your CMS signature container contains a 1.2.840.113549.1.9.5 signingTime signed attribute. This is forbidden for PAdES BASELINE profiles.
I want to change my attach file order in created pdf,attachment are displayed default by name,
how to change them displayed by add time?
this is my implement method:
#Override
public boolean attachFile(String src, String dest, List<SysItemfile> attachmentpaths) {
try {
PdfName name = new PdfName(src);
PdfDocument pdfDoc = new PdfDocument(new PdfReader(src), new PdfWriter(dest));
List<String> descs = new ArrayList<String>();
int i = 0;
int j = 1;
for (SysItemfile attachmentpath : attachmentpaths) {
String filename = attachmentpath.getFilename();
//test for the file name
System.out.println("filename:"+filename);
if (descs.contains(attachmentpath.getFilename())) {
//get the file suffix
String suffix = filename.substring(filename.lastIndexOf(".") + 1);
String realname = filename.substring(0,filename.lastIndexOf("."));
filename = realname+i+"."+suffix;
i++;
} else {
descs.add(attachmentpath.getFilename());
}
PdfFileSpec spec = PdfFileSpec.createEmbeddedFileSpec(pdfDoc, attachmentpath.getFileurl(),
filename, filename, name, name);
// the first parameter is discription
pdfDoc.addFileAttachment(filename, spec);
}
pdfDoc.close();
} catch (IOException e) {
logger.error("attachFile unsuccess!");
logger.error(e.getLocalizedMessage());
return false;
}
return true;
}
After that , when i add attachment to my pdf,the cann't change the order of attachment display.
what should I do?
As long as you only add attachments, the PDF standard does not allow for prescribing the sort order a PDF viewer uses when displaying the attachments.
If, on the other hand, you make the PDF a portable collection (aka a Portfolio), you can prescribe a schema (i.e. the fields in the detail list) and the sort order (by one or a combination of those fields).
You can quite easily make your PDF with attachments a portable collection with the name and modification date sorted by the latter like this:
try ( PdfReader reader = new PdfReader(...);
PdfWriter writer = new PdfWriter(...);
PdfDocument document = new PdfDocument(reader, writer)) {
PdfCollection collection = new PdfCollection();
document.getCatalog().setCollection(collection);
PdfCollectionSchema schema = new PdfCollectionSchema();
PdfCollectionField field = new PdfCollectionField("File Name", PdfCollectionField.FILENAME);
field.setOrder(0);
schema.addField("Name", field);
field = new PdfCollectionField("Modification Date", PdfCollectionField.MODDATE);
field.setOrder(1);
schema.addField("Modified", field);
collection.setSchema(schema);
PdfCollectionSort sort = new PdfCollectionSort("Modified");
collection.setSort(sort);
}
(SortAttachments test testAttachLikeGeologistedWithCollection)
You actually even can define a custom field using a type PdfCollectionField.TEXT, PdfCollectionField.DATE, or PdfCollectionField.NUMBER by which to sort. You can set the value of such a custom field on a PdfFileSpec via its setCollectionItem method.
I am writing a service that receives PKCS7 data (extracted from signed PDF documents) and needs to verify it.
I am using iText7 PdfPKCS7 for that, but the signature verification always fails. I can read all other information from the PKCS7 (certificates, timestamps etc., I have verified that also with OpenSSL). Only the signature appears as invalid.
Here's the test case:
public static void main(String[] args) throws IOException, GeneralSecurityException,
NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Logger logger = Logger.getLogger(PKCS7Test.class.getName());
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
String path ="/tmp/signed.pdf";
PdfDocument pdf = new PdfDocument(new PdfReader(path));
SignatureUtil signatureUtil = new SignatureUtil(pdf);
List<String> names = signatureUtil.getSignatureNames();
String outerRevisionName = names.get(names.size()-1);
PdfPKCS7 pkcs7In = signatureUtil.verifySignature(outerRevisionName);
boolean isValidSignature = pkcs7In.verify();
logger.log(Level.INFO, "pkcs7In signature is " + ((isValidSignature)?"":"not ") + "valid");
// get hash of original document
Field digestAttrField = PdfPKCS7.class.getDeclaredField("digestAttr");
digestAttrField.setAccessible(true);
byte[] originalDigest = (byte[]) digestAttrField.get(pkcs7In);
// get pkcs7 structure of original signature
PdfDictionary dict = signatureUtil.getSignatureDictionary(outerRevisionName);
PdfString contents = dict.getAsString(PdfName.Contents);
byte [] originalBytes = contents.getValueBytes();
String originalPkcs7 = Base64.getEncoder().encodeToString(originalBytes);
// now reverse process and import PKCS7 data back into object
byte[] pkcs7Bytes = Base64.getDecoder().decode(originalPkcs7);
PdfPKCS7 pkcs7Out = new PdfPKCS7(pkcs7Bytes, PdfName.Adbe_pkcs7_detached, provider.getName());
isValidSignature = pkcs7Out.verify();
logger.log(Level.INFO, "pkcs7Out signature is " + ((isValidSignature)?"":"not ") + "valid");
// get hash of original document from imported signature
digestAttrField = PdfPKCS7.class.getDeclaredField("digestAttr");
digestAttrField.setAccessible(true);
byte [] importedDigest = (byte[]) digestAttrField.get(pkcs7Out);
logger.log(Level.INFO, "Hash values are " + ((Arrays.areEqual(originalDigest, importedDigest))?"":"not ") + "equal");
The output is invariably:
pkcs7In signature is valid
pkcs7Out signature is not valid
Hash values are equal
I suppose I'm doing something wrong with the import, but just can't find out what...
Btw. /tmp/signed.pdf validates OK (signature and content) in other PDF tools (Adobe DC, PdfOnline etc.)
Edit:
I tried to verify the signature with BouncyCastle, which fails too ...
CMSSignedData signature = new CMSSignedData(pkcs7Bytes);
Store certStore = signature.getCertificates();
SignerInformationStore signers = signature.getSignerInfos();
Collection c = signers.getSigners();
Iterator it = c.iterator();
while (it.hasNext())
{
SignerInformation signer = (SignerInformation)it.next();
Collection certCollection = certStore.getMatches(signer.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder cert = (X509CertificateHolder)certIt.next();
try {
signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert));
}
catch (Exception ex) {
logger.log(Level.INFO, "Failed to verify with: " + cert.getSubject().toString() + ": " + ex.getLocalizedMessage());
byte [] contentDigest = signer.getContentDigest();
bi = new BigInteger(1, contentDigest);
String hash = String.format("%0" + (contentDigest.length << 1) + "x", bi);
logger.log(Level.INFO, "Bouncycastle Hash from pkcs7Out is {0}", hash);
}
}
The result is Failed to verify with: ... message-digest attribute value does not match calculated value, and the hash value obviously differs from the real one...
Edit 2:
Workaround by adding separate verify(byte[] msgDigestBytes) method to PdfPKCS7:
public boolean verify(final byte[] msgDigestBytes) throws GeneralSecurityException {
if (verified)
return verifyResult;
if (isTsp) {
TimeStampTokenInfo info = timeStampToken.getTimeStampInfo();
MessageImprint imprint = info.toASN1Structure().getMessageImprint();
byte[] md = msgDigestBytes; // was: messageDigest.digest();
byte[] imphashed = imprint.getHashedMessage();
verifyResult = Arrays.equals(md, imphashed);
} else {
if (sigAttr != null || sigAttrDer != null) {
// was: final byte[] msgDigestBytes = messageDigest.digest();
boolean verifyRSAdata = true;
// Stefan Santesson fixed a bug, keeping the code backward compatible
boolean encContDigestCompare = false;
if (rsaData != null) {
verifyRSAdata = Arrays.equals(msgDigestBytes, rsaData);
encContDigest.update(rsaData);
encContDigestCompare = Arrays.equals(encContDigest.digest(), digestAttr);
}
boolean absentEncContDigestCompare = Arrays.equals(msgDigestBytes, digestAttr);
boolean concludingDigestCompare = absentEncContDigestCompare || encContDigestCompare;
boolean sigVerify = verifySigAttributes(sigAttr) || verifySigAttributes(sigAttrDer);
verifyResult = concludingDigestCompare && sigVerify && verifyRSAdata;
} else {
if (rsaData != null)
sig.update(msgDigestBytes); // was: sig.update(messageDigest.digest());
verifyResult = sig.verify(digest);
}
}
verified = true;
return verifyResult;
}
For this to work, I need the originally signed hash of the document from a trusted source, which in my case I have. Does this still verify that the signature made over the hash is correct?
From
PdfPKCS7 pkcs7Out = new PdfPKCS7(pkcs7Bytes, PdfName.Adbe_pkcs7_detached, provider.getName());
isValidSignature = pkcs7Out.verify();
you cannot expect a proper validation result of the signature: This PdfPKCS7 only knows the CMS signature container, the signature SubFilter, and the security provider to provide algorithm implementations. Thus, it has no information on the very PDF the signature actually is meant to sign. So that piece of code has no means to validate the signature in question, in particular not whether it properly signs its alleged signed data!
If you want to validate the signature using that PdfPKCS7 object, you have to finish initializing it so it does have the required information from the PDF.
To see what is required have a look at the SignatureUtil method verifySignature:
PdfPKCS7 pk = null;
if (sub.equals(PdfName.Adbe_x509_rsa_sha1)) {
PdfString cert = signature.getPdfObject().getAsString(PdfName.Cert);
if (cert == null)
cert = signature.getPdfObject().getAsArray(PdfName.Cert).getAsString(0);
pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), cert.getValueBytes(), provider);
}
else
pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), sub, provider);
updateByteRange(pk, signature);
PdfString date = signature.getDate();
if (date != null)
pk.setSignDate(PdfDate.decode(date.toString()));
String signName = signature.getName();
pk.setSignName(signName);
String reason = signature.getReason();
if (reason != null)
pk.setReason(reason);
String location = signature.getLocation();
if (location != null)
pk.setLocation(location);
Thus, you have to
update the digest of the signed data like SignatureUtil.updateByteRange does; this is the step that informs the PdfPKCS7 object about the actually signed data to allow actual validation; and
copy several pieces of information from the signature dictionary to the PdfPKCS7 object; for validation purposes in particular the signing time may be of interest.
I want to retrieve the OCSP information from a given X509Certificate object. Since I don't know how to parse this information I am asking this question here.
Here is what I got so far:
X509Certificate x509cert = ... //The Certificate
ASN1Primitive obj = ASN1Primitive.fromByteArray(x509cert
.getExtensionValue(Extension.authorityInfoAccess
.getId()));
AuthorityInformationAccess aia = AuthorityInformationAccess.getInstance(obj);
an exception is thrown:
java.security.cert.CertificateParsingException: java.lang.IllegalArgumentException: unknown object in getInstance: org.bouncycastle.asn1.DEROctetString
How do I parse a ASN1Primitive into a valid DEROctetString to progress?
The value of the ASN1Primitive in the example is:
3032303006082b060105050730018624687474703a2f2f6f6373702e616368656c6f732e64653a383038302f6f6373702f65676b
and seems to me to be a valid value.
You can build a DEROctetString using ASN1InputStream
byte[] authInfoAccessExtensionValue = x509cert.getExtensionValue(X509Extension.authorityInfoAccess.getId());
ASN1InputStream ais1 = new ASN1InputStream(new ByteArrayInputStream(authInfoAccessExtensionValue ));
DEROctetString oct = (DEROctetString) (ais1.readObject());
ASN1InputStream ais2 = new ASN1InputStream(oct.getOctets());
AuthorityInformationAccess authorityInformationAccess = AuthorityInformationAccess.getInstance(ais2.readObject());
Try this code to get the OCSP URI meta-data within a X509Certificate. The code is extracted from class OnlineOCSPSource of SD-DSS project (and slightly modified)
public String getAccessLocation(X509Certificate certificate) throws IOException {
final ASN1ObjectIdentifier ocspAccessMethod = X509ObjectIdentifiers.ocspAccessMethod;
final byte[] authInfoAccessExtensionValue = certificate.getExtensionValue(X509Extension.authorityInfoAccess.getId());
if (null == authInfoAccessExtensionValue) {
return null;
}
ASN1InputStream ais1 = null;
ASN1InputStream ais2 = null;
try {
final ByteArrayInputStream bais = new ByteArrayInputStream(authInfoAccessExtensionValue);
ais1 = new ASN1InputStream(bais);
final DEROctetString oct = (DEROctetString) (ais1.readObject());
ais2 = new ASN1InputStream(oct.getOctets());
final AuthorityInformationAccess authorityInformationAccess = AuthorityInformationAccess.getInstance(ais2.readObject());
final AccessDescription[] accessDescriptions = authorityInformationAccess.getAccessDescriptions();
for (AccessDescription accessDescription : accessDescriptions) {
final boolean correctAccessMethod = accessDescription.getAccessMethod().equals(ocspAccessMethod);
if (!correctAccessMethod) {
continue;
}
final GeneralName gn = accessDescription.getAccessLocation();
if (gn.getTagNo() != GeneralName.uniformResourceIdentifier) {
//Not a uniform resource identifier
continue;
}
final DERIA5String str = (DERIA5String) ((DERTaggedObject) gn.toASN1Primitive()).getObject();
final String accessLocation = str.getString();
return accessLocation;
}
return null;
} finally {
ais1.close();
ais2.close();
}
}
In Bouncy Castle 1.57, there's no need to create the intermediary ASN1Primitive. You can get the extension just by using the org.bouncycastle.asn1.x509.Extension and org.bouncycastle.x509.extension.X509ExtensionUtil classes:
X509Certificate cert = // the certificate
// get Authority Information Access extension
byte[] extVal = cert.getExtensionValue(Extension.authorityInfoAccess.getId());
AuthorityInformationAccess aia = AuthorityInformationAccess.getInstance(X509ExtensionUtil.fromExtensionValue(extVal));
Then you can use the aia object:
AccessDescription[] descriptions = aia.getAccessDescriptions();
for (AccessDescription ad : descriptions) {
// ...
}
You can do it in previous versions as well, but for version <= 1.47 I believe that Extension class doesn't exist and you should use org.bouncycastle.asn1.x509.X509Extension instead (I think that org.bouncycastle.x509.extension.X509ExtensionUtil is the same).