diff options
| author | Thomas <> | 2024-09-05 09:55:35 +0200 |
|---|---|---|
| committer | Thomas <> | 2024-09-05 09:55:35 +0200 |
| commit | d46ce27508d11be00ce247457f30e5381e7d280b (patch) | |
| tree | 7621bc717a798d1889439064c062ec29cd2d45d9 | |
| parent | dbdee1a6a73053602d4f8ff0817ac3e9ecf7911f (diff) | |
| download | pdf-as-4-d46ce27508d11be00ce247457f30e5381e7d280b.tar.gz pdf-as-4-d46ce27508d11be00ce247457f30e5381e7d280b.tar.bz2 pdf-as-4-d46ce27508d11be00ce247457f30e5381e7d280b.zip | |
fix(verify): broken signature verification if documents contains a signature-field that was not used yet
| -rw-r--r-- | pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/settings/IProfileConstants.java | 2 | ||||
| -rw-r--r-- | pdf-as-pdfbox-2/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/pdfbox2/PDFBOXVerifier.java | 173 | ||||
| -rw-r--r-- | pdf-as-web/build.gradle | 1 | ||||
| -rw-r--r-- | pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/SimpleVerifyServletTest.java | 164 | ||||
| -rw-r--r-- | pdf-as-web/src/test/resources/data/dummy-pdf-signed.pdf | bin | 0 -> 167548 bytes | |||
| -rw-r--r-- | pdf-as-web/src/test/resources/data/placeholder_sigfield_and_qr.pdf | bin | 0 -> 181919 bytes |
6 files changed, 272 insertions, 68 deletions
diff --git a/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/settings/IProfileConstants.java b/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/settings/IProfileConstants.java index 95eaa8ea..5adf320f 100644 --- a/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/settings/IProfileConstants.java +++ b/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/settings/IProfileConstants.java @@ -92,7 +92,7 @@ public interface IProfileConstants { public final static String SIGNFIELD_VALUE = "adobeSignFieldValue"; public final static String TIMEZONE_BASE = "timezone"; public final static String SIG_PDFA1B_VALID = "SIG_PDFA1B_VALID"; - public final static String SIG_PDFA_VALID = "SIG_PDFA_VALID"; + public final static String SIG_PDF A_VALID = "SIG_PDFA_VALID"; public final static String SIG_PDFUA_FORCE = "SIG_PDFUA_FORCE"; public final static String SIG_NEWPAGE_FORCE = "SIGNED_NEWPAGE_FORCE"; public final static String LATIN1_ENCODING = "latin1_encoding"; diff --git a/pdf-as-pdfbox-2/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/pdfbox2/PDFBOXVerifier.java b/pdf-as-pdfbox-2/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/pdfbox2/PDFBOXVerifier.java index 270e9e28..1fab2793 100644 --- a/pdf-as-pdfbox-2/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/pdfbox2/PDFBOXVerifier.java +++ b/pdf-as-pdfbox-2/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/pdfbox2/PDFBOXVerifier.java @@ -33,7 +33,7 @@ public class PDFBOXVerifier implements VerifyBackend { @Override public List<VerifyResult> verify(VerifyParameter parameter) throws PDFASError { int signatureToVerify = parameter.getWhichSignature(); - int currentSignature = 0; + PDDocument doc = null; try { List<VerifyResult> result = new ArrayList<VerifyResult>(); @@ -62,97 +62,136 @@ public class PDFBOXVerifier implements VerifyBackend { return result; } - int lastSig = -1; - for (int i = 0; i < fields.size(); i++) { - COSDictionary field = (COSDictionary) fields.getObject(i); - String type = field.getNameAsString("FT"); - if ("Sig".equals(type)) { - lastSig = i; - } - } - + int lastSig = selectLastSigIndex(fields); byte[] inputData = IOUtils.toByteArray(parameter.getDataSource().getInputStream()); + int currentSignature = 0; for (int i = 0; i < fields.size(); i++) { COSDictionary field = (COSDictionary) fields.getObject(i); String type = field.getNameAsString("FT"); - if ("Sig".equals(type)) { - boolean verifyThis = true; - - if (signatureToVerify >= 0) { - // verify only specific siganture! - verifyThis = signatureToVerify == currentSignature; - } - - if (signatureToVerify == -2) { - verifyThis = i == lastSig; - } - - if (verifyThis) { - logger.trace("Found Signature: "); + if ("Sig".equals(type)) { + if (verifyCurrentSig(signatureToVerify, i, lastSig, currentSignature)) { + logger.trace("Found Signature Form: "); COSBase base = field.getDictionaryObject("V"); - COSDictionary dict = (COSDictionary) base; - - logger.debug("Signer: " + dict.getNameAsString("Name")); - logger.debug("SubFilter: " + dict.getNameAsString("SubFilter")); - logger.debug("Filter: " + dict.getNameAsString("Filter")); - logger.debug("Modified: " + dict.getNameAsString("M")); - COSArray byteRange = (COSArray) dict.getDictionaryObject("ByteRange"); - - StringBuilder sb = new StringBuilder(); - int[] bytes = new int[byteRange.size()]; - for (int j = 0; j < byteRange.size(); j++) { - bytes[j] = byteRange.getInt(j); - sb.append(" " + bytes[j]); - } - - logger.debug("ByteRange" + sb.toString()); - - COSString content = (COSString) dict.getDictionaryObject("Contents"); - - ByteArrayOutputStream contentData = new ByteArrayOutputStream(); - for (int j = 0; j < bytes.length; j = j + 2) { - int offset = bytes[j]; - int length = bytes[j + 1]; - - contentData.write(inputData, offset, length); - } - contentData.close(); - - IVerifyFilter verifyFilter = verifier.getVerifier(dict.getNameAsString("Filter"), - dict.getNameAsString("SubFilter")); - - IVerifier lvlVerifier = verifier.getVerifierByLevel(parameter.getSignatureVerificationLevel()); - synchronized (lvlVerifier) { - lvlVerifier.setConfiguration(parameter.getConfiguration()); - if (verifyFilter != null) { - List<VerifyResult> results = verifyFilter.verify(contentData.toByteArray(), - content.getBytes(), parameter.getVerificationTime(), bytes, lvlVerifier); - if (results != null && !results.isEmpty()) { - result.addAll(results); - } - } + if (base != null) { + checkTechicalSig(base, inputData, verifier, parameter, result, i); + + } else { + logger.info("Skipping signature form, because it looks empty"); + } + } - currentSignature++; + + currentSignature++; } - } + } return result; + } catch (IOException e) { logger.warn("Failed to verify document", e); throw ErrorExtractor.searchPdfAsError(e, null); + } catch (PdfAsException e) { logger.warn("Failed to verify document", e); throw ErrorExtractor.searchPdfAsError(e, null); + } finally { if (doc != null) { try { doc.close(); + } catch (IOException e) { logger.info("Failed to close doc"); + } } } } + private boolean verifyCurrentSig(int signatureToVerify, int i, int lastSig, int currentSignature) { + if (signatureToVerify >= 0) { + // verify only specific siganture! + return signatureToVerify == currentSignature; + + } + + if (signatureToVerify == -2) { + return i == lastSig; + + } + + return true; + } + + private int selectLastSigIndex(COSArray fields) { + int lastSig = -1; + for (int i = 0; i < fields.size(); i++) { + COSDictionary field = (COSDictionary) fields.getObject(i); + String type = field.getNameAsString("FT"); + if ("Sig".equals(type)) { + lastSig = i; + } + } + + return lastSig; + + } + + private void checkTechicalSig(COSBase base, byte[] inputData, VerifierDispatcher verifier, VerifyParameter parameter, + List<VerifyResult> result, int i) throws IOException, PdfAsException { + try { + COSDictionary dict = (COSDictionary) base; + + logger.debug("Signer: " + dict.getNameAsString("Name")); + logger.debug("SubFilter: " + dict.getNameAsString("SubFilter")); + logger.debug("Filter: " + dict.getNameAsString("Filter")); + logger.debug("Modified: " + dict.getNameAsString("M")); + COSArray byteRange = (COSArray) dict.getDictionaryObject("ByteRange"); + + StringBuilder sb = new StringBuilder(); + int[] bytes = new int[byteRange.size()]; + for (int j = 0; j < byteRange.size(); j++) { + bytes[j] = byteRange.getInt(j); + sb.append(" " + bytes[j]); + } + + logger.debug("ByteRange" + sb.toString()); + + COSString content = (COSString) dict.getDictionaryObject("Contents"); + + ByteArrayOutputStream contentData = new ByteArrayOutputStream(); + for (int j = 0; j < bytes.length; j = j + 2) { + int offset = bytes[j]; + int length = bytes[j + 1]; + + contentData.write(inputData, offset, length); + } + contentData.close(); + + IVerifyFilter verifyFilter = verifier.getVerifier(dict.getNameAsString("Filter"), + dict.getNameAsString("SubFilter")); + + IVerifier lvlVerifier = verifier.getVerifierByLevel(parameter.getSignatureVerificationLevel()); + synchronized (lvlVerifier) { + lvlVerifier.setConfiguration(parameter.getConfiguration()); + if (verifyFilter != null) { + List<VerifyResult> results = verifyFilter.verify(contentData.toByteArray(), + content.getBytes(), parameter.getVerificationTime(), bytes, lvlVerifier); + if (results != null && !results.isEmpty()) { + result.addAll(results); + } + } + } + + } catch (NullPointerException e) { + logger.info("Verification of signature #{} failed with generic error", i); + } + + } + } + + + + diff --git a/pdf-as-web/build.gradle b/pdf-as-web/build.gradle index 067b9a96..fdf18d9e 100644 --- a/pdf-as-web/build.gradle +++ b/pdf-as-web/build.gradle @@ -74,6 +74,7 @@ dependencies { api group: 'javax.jws', name: 'javax.jws-api', version: '1.1' compileOnly 'javax.servlet:javax.servlet-api:3.0.1' testImplementation 'org.springframework:spring-test:5.3.31' + testImplementation 'org.springframework:spring-web:5.3.31' } diff --git a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/SimpleVerifyServletTest.java b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/SimpleVerifyServletTest.java new file mode 100644 index 00000000..a041f102 --- /dev/null +++ b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/SimpleVerifyServletTest.java @@ -0,0 +1,164 @@ +package at.gv.egiz.pdfas.web.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Enumeration; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import at.gv.egiz.pdfas.common.exceptions.PdfAsSettingsValidationException; +import at.gv.egiz.pdfas.common.settings.ISettings; +import at.gv.egiz.pdfas.lib.api.PdfAsFactory; +import at.gv.egiz.pdfas.web.config.WebConfiguration; +import at.gv.egiz.pdfas.web.filter.UserAgentFilter; +import at.gv.egiz.pdfas.web.helper.PdfAsHelper; +import at.gv.egiz.pdfas.web.servlets.VerifyServlet; +import at.gv.egiz.pdfas.web.stats.StatisticEvent; +import at.gv.egiz.pdfas.web.stats.StatisticEvent.Operation; +import at.gv.egiz.pdfas.web.stats.StatisticEvent.Source; +import lombok.SneakyThrows; + +//@Ignore +@RunWith(BlockJUnit4ClassRunner.class) +public class SimpleVerifyServletTest { + + + @BeforeClass + public static void classInitializer() throws IOException { + final String current = new java.io.File(".").getCanonicalPath(); + System.setProperty("pdf-as-web.conf", + current + "/src/test/resources/config/pdfas/pdf-as-web.properties"); + + String webconfig = System.getProperty("pdf-as-web.conf"); + + if(webconfig == null) { + throw new RuntimeException("No web configuration provided!"); + } + + WebConfiguration.configure(webconfig); + PdfAsHelper.init(); + + try { + PdfAsFactory.validateConfiguration((ISettings)PdfAsHelper.getPdfAsConfig()); + + } catch (PdfAsSettingsValidationException e) { + e.printStackTrace(); + } + } + + + @Test + @SneakyThrows + public void unsignedPdf() { + byte[] pdf = IOUtils.toByteArray(SimpleVerifyServletTest.class.getResourceAsStream("/data/enc_own.pdf")); + executeTest(pdf); + + } + + @Test + @SneakyThrows + public void unsignedWithSigField() { + byte[] pdf = IOUtils.toByteArray(SimpleVerifyServletTest.class.getResourceAsStream("/data/placeholder_sigfield_and_qr.pdf")); + executeTest(pdf); + + } + + @Test + @SneakyThrows + public void signedPdf() { + byte[] pdf = IOUtils.toByteArray(SimpleVerifyServletTest.class.getResourceAsStream("/data/dummy-pdf-signed.pdf")); + executeTest(pdf); + + } + + + + + @SneakyThrows + private MockHttpServletResponse executeTest(byte[] pdf) { + MockHttpServletRequest httpReq = new MockHttpServletRequest("POST", "https://localhost/pdfas"); + MockHttpServletResponse httpResp = new MockHttpServletResponse(); + + // perform operation + performTest(httpReq, httpResp, pdf); + + //validate state + assertNotNull("httpResp", httpResp); + assertEquals("httpStatus", 200, httpResp.getStatus()); + String body = httpResp.getContentAsString(); + assertFalse("Empty body", StringUtils.isEmpty(body)); + + return httpResp; + + } + + + private void performTest(HttpServletRequest httpReq, HttpServletResponse httpResp, byte[] pdf) throws NoSuchMethodException, + SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ServletException { + VerifyServlet servlet = new VerifyServlet (); + ServletConfig servletConfig = buildServletConfig(); + servlet.init(servletConfig); + + Method method = servlet.getClass().getDeclaredMethod("doVerify", + HttpServletRequest.class, HttpServletResponse.class, byte[].class, StatisticEvent.class); + method.setAccessible(true); + StatisticEvent statisticEvent = new StatisticEvent(); + statisticEvent.setStartNow(); + statisticEvent.setSource(Source.WEB); + statisticEvent.setOperation(Operation.VERIFY); + statisticEvent.setUserAgent(UserAgentFilter.getUserAgent()); + + method.invoke(servlet, httpReq, httpResp, pdf, statisticEvent); + + } + + + private ServletConfig buildServletConfig() { + + return new ServletConfig() { + + private ServletContext servletContext = null; + + @Override + public String getServletName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public Enumeration<String> getInitParameterNames() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getInitParameter(String name) { + // TODO Auto-generated method stub + return null; + } + }; + } +} diff --git a/pdf-as-web/src/test/resources/data/dummy-pdf-signed.pdf b/pdf-as-web/src/test/resources/data/dummy-pdf-signed.pdf Binary files differnew file mode 100644 index 00000000..e66d621e --- /dev/null +++ b/pdf-as-web/src/test/resources/data/dummy-pdf-signed.pdf diff --git a/pdf-as-web/src/test/resources/data/placeholder_sigfield_and_qr.pdf b/pdf-as-web/src/test/resources/data/placeholder_sigfield_and_qr.pdf Binary files differnew file mode 100644 index 00000000..e7a29895 --- /dev/null +++ b/pdf-as-web/src/test/resources/data/placeholder_sigfield_and_qr.pdf |
