Skip to content

Commit

Permalink
Fix for consiquent signatures verification by preventing copying of e…
Browse files Browse the repository at this point in the history
…xisting RandomAccessSource

DEVSIX-6181
  • Loading branch information
Alexander Kozlov committed Feb 16, 2022
1 parent 3213363 commit 36c2f7a
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,5 @@ public final class IoExceptionMessage {
public static final String IMAGE_MAGICK_OUTPUT_IS_NULL = "ImageMagick process output is null.";
public static final String IMAGE_MAGICK_PROCESS_EXECUTION_FAILED =
"ImageMagick process execution finished with errors: ";
public static final String ALREADY_CLOSED = "Already closed";
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ This file is part of the iText (R) project.
package com.itextpdf.io.source;


import com.itextpdf.io.exceptions.IoExceptionMessage;

/**
* A RandomAccessSource that is based on an underlying byte array
*/
Expand All @@ -53,30 +55,41 @@ class ArrayRandomAccessSource implements IRandomAccessSource {
private byte[] array;

public ArrayRandomAccessSource(byte[] array) {
if(array == null) throw new IllegalArgumentException("Passed byte array can not be null.");
if(array == null) {
throw new IllegalArgumentException("Passed byte array can not be null.");
}
this.array = array;
}

public int get(long offset) {
if (offset >= array.length) return -1;
if (array == null) {
throw new IllegalStateException(IoExceptionMessage.ALREADY_CLOSED);
}
if (offset >= array.length) {
return -1;
}
return 0xff & array[(int)offset];
}

public int get(long offset, byte[] bytes, int off, int len) {
if (array == null) throw new IllegalStateException("Already closed");

if (offset >= array.length)
if (array == null) {
throw new IllegalStateException(IoExceptionMessage.ALREADY_CLOSED);
}
if (offset >= array.length) {
return -1;

if (offset + len > array.length)
}
if (offset + len > array.length) {
len = (int)(array.length - offset);

}
System.arraycopy(array, (int)offset, bytes, off, len);

return len;
}

public long length() {
if (array == null) {
throw new IllegalStateException(IoExceptionMessage.ALREADY_CLOSED);
}
return array.length;
}

Expand Down
9 changes: 9 additions & 0 deletions io/src/main/java/com/itextpdf/io/source/RASInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ public RASInputStream(IRandomAccessSource source){
this.source = source;
}

/**
* Gets the source
*
* @return an instance of {@link IRandomAccessSource}
*/
public IRandomAccessSource getSource() {
return source;
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,37 @@ public IRandomAccessSource createSource(URL url) throws java.io.IOException{
}

/**
* Creates a {@link IRandomAccessSource} based on an {@link InputStream}. The full content of the InputStream is read into memory and used
* Creates or extracts a {@link IRandomAccessSource} based on an {@link InputStream}.
*
* <p>
* If the InputStream is an instance of {@link RASInputStream} then extracts the source from it.
* Otherwise The full content of the InputStream is read into memory and used
* as the source for the {@link IRandomAccessSource}
*
* @param inputStream the stream to read from
*
* @return the newly created or extracted {@link IRandomAccessSource}
*
* @throws java.io.IOException in case of any I/O error.
*/
public IRandomAccessSource extractOrCreateSource(InputStream inputStream) throws java.io.IOException {
if (inputStream instanceof RASInputStream) {
return ((RASInputStream) inputStream).getSource();
}
return createSource(StreamUtil.inputStreamToArray(inputStream));
}

/**
* Creates a {@link IRandomAccessSource} based on an {@link InputStream}.
*
* <p>
* The full content of the InputStream is read into memory and used
* as the source for the {@link IRandomAccessSource}
*
* @param inputStream the stream to read from
*
* @return the newly created {@link IRandomAccessSource}
*
* @throws java.io.IOException in case of any I/O error.
*/
public IRandomAccessSource createSource(InputStream inputStream) throws java.io.IOException{
Expand All @@ -169,7 +196,6 @@ public IRandomAccessSource createBestSource(String filename) throws java.io.IOEx
|| filename.startsWith("https://")
|| filename.startsWith("jar:")
|| filename.startsWith("wsjar:")
|| filename.startsWith("wsjar:")
|| filename.startsWith("vfszip:")) {
return createSource(new URL(filename));
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2022 iText Group NV
Authors: iText Software.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License version 3
as published by the Free Software Foundation with the addition of the
following permission added to Section 15 as permitted in Section 7(a):
FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
OF THIRD PARTY RIGHTS
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses or write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA, 02110-1301 USA, or download the license from the following URL:
http://itextpdf.com/terms-of-use/
The interactive user interfaces in modified source and object code versions
of this program must display Appropriate Legal Notices, as required under
Section 5 of the GNU Affero General Public License.
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the producer line in every PDF that is created
or manipulated using iText.
You can be released from the requirements of the license by purchasing
a commercial license. Buying such a license is mandatory as soon as you
develop commercial activities involving the iText software without
disclosing the source code of your own applications.
These activities include: offering paid services to customers as an ASP,
serving PDFs on the fly in a web application, shipping iText with a closed
source product.
For more information, please contact iText Software Corp. at this
address: sales@itextpdf.com
*/
package com.itextpdf.io.source;

import com.itextpdf.io.exceptions.IoExceptionMessage;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.annotations.type.UnitTest;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Assert;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(UnitTest.class)
public class RandomAccessSourceFactoryTest extends ExtendedITextTest {

private final static String SOURCE_FILE = "./src/test/resources/com/itextpdf/io/source/RAF.txt";

@Test
public void readRASInputStreamClosedTest() throws IOException {
String fileName = SOURCE_FILE;
try (InputStream pdfStream = new FileInputStream(fileName)) {

IRandomAccessSource randomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(pdfStream);
RASInputStream rasInputStream = new RASInputStream(randomAccessSource);
IRandomAccessSource extractedRandomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(rasInputStream);

extractedRandomAccessSource.close();

Exception e = Assert.assertThrows(IllegalStateException.class, () -> rasInputStream.read());
Assert.assertEquals(IoExceptionMessage.ALREADY_CLOSED, e.getMessage());

e = Assert.assertThrows(IllegalStateException.class,
() -> randomAccessSource.get(0));
Assert.assertEquals(IoExceptionMessage.ALREADY_CLOSED, e.getMessage());
e = Assert.assertThrows(IllegalStateException.class,
() -> randomAccessSource.get(0, new byte[10], 0, 10));
Assert.assertEquals(IoExceptionMessage.ALREADY_CLOSED, e.getMessage());
e = Assert.assertThrows(IllegalStateException.class,
() -> randomAccessSource.length());
Assert.assertEquals(IoExceptionMessage.ALREADY_CLOSED, e.getMessage());
}
}

@Test
public void readRASInputStreamTest() throws IOException {
String fileName = SOURCE_FILE;
try (InputStream pdfStream = new FileInputStream(fileName)) {
IRandomAccessSource randomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(pdfStream);
RASInputStream rasInputStream = new RASInputStream(randomAccessSource);
IRandomAccessSource extractedRandomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(rasInputStream);

Assert.assertEquals(72, rasInputStream.read());
Assert.assertEquals(72, extractedRandomAccessSource.get(0));
Assert.assertEquals(extractedRandomAccessSource, rasInputStream.getSource());
}
}
}
15 changes: 10 additions & 5 deletions kernel/src/main/java/com/itextpdf/kernel/pdf/PdfReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ This file is part of the iText (R) project.
import com.itextpdf.io.source.ByteUtils;
import com.itextpdf.io.source.IRandomAccessSource;
import com.itextpdf.io.source.PdfTokenizer;
import com.itextpdf.io.source.RASInputStream;
import com.itextpdf.io.source.RandomAccessFileOrArray;
import com.itextpdf.io.source.RandomAccessSourceFactory;
import com.itextpdf.io.source.WindowRandomAccessSource;
Expand Down Expand Up @@ -138,13 +139,15 @@ public PdfReader(IRandomAccessSource byteSource, ReaderProperties properties) th
/**
* Reads and parses a PDF document.
*
* @param is the {@code InputStream} containing the document. The stream is read to the
* end but is not closed.
* @param is the {@code InputStream} containing the document. If the inputStream is an instance of
* {@link RASInputStream} then the {@link IRandomAccessSource} would be extracted. Otherwise the stream
* is read to the end but is not closed.
* @param properties properties of the created reader
*
* @throws IOException on error
*/
public PdfReader(InputStream is, ReaderProperties properties) throws IOException {
this(new RandomAccessSourceFactory().createSource(is), properties, true);
this(new RandomAccessSourceFactory().extractOrCreateSource(is), properties, true);
}

/**
Expand All @@ -161,8 +164,10 @@ public PdfReader(java.io.File file) throws FileNotFoundException, IOException {
/**
* Reads and parses a PDF document.
*
* @param is the {@code InputStream} containing the document. the {@code InputStream} containing the document. The stream is read to the
* end but is not closed.
* @param is the {@code InputStream} containing the document. If the inputStream is an instance of
* {@link RASInputStream} then the {@link IRandomAccessSource} would be extracted. Otherwise the stream
* is read to the end but is not closed.
*
* @throws IOException on error
*/
public PdfReader(InputStream is) throws IOException {
Expand Down
49 changes: 49 additions & 0 deletions kernel/src/test/java/com/itextpdf/kernel/pdf/PdfReaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ This file is part of the iText (R) project.

import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.exceptions.IoExceptionMessage;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.io.source.ByteUtils;
import com.itextpdf.io.source.IRandomAccessSource;
import com.itextpdf.io.source.RASInputStream;
import com.itextpdf.io.source.RandomAccessSourceFactory;
import com.itextpdf.kernel.exceptions.InvalidXRefPrevException;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
Expand Down Expand Up @@ -2223,6 +2225,53 @@ public void closingArrayBracketMissingConservativeTest() throws IOException {
exception.getCause().getMessage());
}

@Test
public void readRASInputStreamClosedTest() throws IOException {
String fileName = SOURCE_FOLDER + "hello.pdf";
try (InputStream pdfStream = new FileInputStream(fileName)) {

IRandomAccessSource randomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(pdfStream);
RASInputStream rasInputStream = new RASInputStream(randomAccessSource);

randomAccessSource.close();

Exception e = Assert.assertThrows(IllegalStateException.class,
() -> new PdfReader(rasInputStream));
Assert.assertEquals(IoExceptionMessage.ALREADY_CLOSED, e.getMessage());
}
}

@Test
public void readRASInputStreamTest() throws IOException {
String fileName = SOURCE_FOLDER + "hello.pdf";
try (InputStream pdfStream = new FileInputStream(fileName)) {
IRandomAccessSource randomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(pdfStream);
RASInputStream rasInputStream = new RASInputStream(randomAccessSource);

try (PdfReader reader = new PdfReader(rasInputStream)) {
randomAccessSource.close();
Exception e = Assert.assertThrows(IllegalStateException.class, () -> new PdfDocument(reader));
Assert.assertEquals(IoExceptionMessage.ALREADY_CLOSED, e.getMessage());
}
}
}

@Test
public void readRASInputStreamValidTest() throws IOException {
String fileName = SOURCE_FOLDER + "hello.pdf";
try (InputStream pdfStream = new FileInputStream(fileName)) {
IRandomAccessSource randomAccessSource = new RandomAccessSourceFactory()
.extractOrCreateSource(pdfStream);
RASInputStream rasInputStream = new RASInputStream(randomAccessSource);

try (PdfReader reader = new PdfReader(rasInputStream)) {
AssertUtil.doesNotThrow(() -> new PdfDocument(reader));
}
}
}

private static File copyFileForTest(String fileName, String copiedFileName) throws IOException {
File copiedFile = new File(copiedFileName);
Files.copy(Paths.get(fileName), Paths.get(copiedFileName));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ This file is part of the iText (R) project.
public class LtvVerifierIntegrationTest extends ExtendedITextTest {
private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/signatures/LtvVerifierIntegrationTest/";


@BeforeClass
public static void before() {
Security.addProvider(new BouncyCastleProvider());
Expand Down Expand Up @@ -262,4 +263,23 @@ public void notTrustedRootCertificateInLatestRevisionTest()
Assert.assertEquals("Root certificate passed without checking", verificationOK.message);
}
}

@Test
public void switchBetweenSeveralRevisionsTest() throws IOException, GeneralSecurityException {
String testInput = SOURCE_FOLDER + "severalConsequentSignatures.pdf";

try(PdfReader pdfReader = new PdfReader(testInput); PdfDocument pdfDoc = new PdfDocument(pdfReader)) {

LtvVerifier ltvVerifier = new LtvVerifier(pdfDoc);

Assert.assertEquals("timestampSig2", ltvVerifier.signatureName);
ltvVerifier.switchToPreviousRevision();
Assert.assertEquals("Signature2", ltvVerifier.signatureName);
ltvVerifier.switchToPreviousRevision();
Assert.assertEquals("timestampSig1", ltvVerifier.signatureName);
ltvVerifier.switchToPreviousRevision();
Assert.assertEquals("Signature1", ltvVerifier.signatureName);
ltvVerifier.switchToPreviousRevision();
}
}
}
Binary file not shown.

0 comments on commit 36c2f7a

Please sign in to comment.