ASN.1 BER data value reader

This commit adds an ASN.1 Basic Encoding Rules (BER) data value reader
which returns data values, one by one, from a source (e.g., ByteBuffer
or InputStream containing the data values in their BER encoded form).
The interpretation of the data values (e.g., how to obtain a numeric
value from an INTEGER data value, or how to extract the elements of a
SEQUENCE data value) is left to callers of the BER data value reader.

This commit also adds a number of AllTests classes which organize all
test classes into one large test suite. The reason is Bazel which makes
it hard to simply run all tests in a subtree without having to pull in
third-party dependencies. A finer-grained alternative is to create a
java_test target per test class, but that requires creating
java_library targets for shared classes, such as test utils, and this
in turn requires adding an explicit dependency on junit4 which is a
pain because there's no portable way to specify this.

Test: bazel test ...
Test: gradlew test
Bug: 31517633
Change-Id: Ie14647f877378f5fd1aa35dc36c5d682aa364e89
diff --git a/BUILD b/BUILD
index a836f2f..10d3c0e 100644
--- a/BUILD
+++ b/BUILD
@@ -24,9 +24,10 @@
 )
 
 java_test(
-    name = "ApkUtilsTest",
-    srcs = [
-        "src/test/java/com/android/apksig/apk/ApkUtilsTest.java",
-    ],
+    name = "all",
+    srcs = glob([
+        "src/test/java/com/android/apksig/**/*.java",
+    ]),
+    test_class = "com.android.apksig.AllTests",
     deps = [":apksig"],
 )
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java
new file mode 100644
index 0000000..f5604ff
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
+ */
+public class BerDataValue {
+    private final ByteBuffer mEncoded;
+    private final ByteBuffer mEncodedContents;
+    private final int mTagClass;
+    private final boolean mConstructed;
+    private final int mTagNumber;
+
+    BerDataValue(
+            ByteBuffer encoded,
+            ByteBuffer encodedContents,
+            int tagClass,
+            boolean constructed,
+            int tagNumber) {
+        mEncoded = encoded;
+        mEncodedContents = encodedContents;
+        mTagClass = tagClass;
+        mConstructed = constructed;
+        mTagNumber = tagNumber;
+    }
+
+    /**
+     * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
+     * constants.
+     */
+    public int getTagClass() {
+        return mTagClass;
+    }
+
+    /**
+     * Returns {@code true} if the content octets of this data value are the complete BER encoding
+     * of one or more data values, {@code false} if the content octets of this data value directly
+     * represent the value.
+     */
+    public boolean isConstructed() {
+        return mConstructed;
+    }
+
+    /**
+     * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
+     * constants.
+     */
+    public int getTagNumber() {
+        return mTagNumber;
+    }
+
+    /**
+     * Returns the encoded form of this data value.
+     */
+    public ByteBuffer getEncoded() {
+        return mEncoded.slice();
+    }
+
+    /**
+     * Returns the encoded contents of this data value.
+     */
+    public ByteBuffer getEncodedContents() {
+        return mEncodedContents.slice();
+    }
+
+    /**
+     * Returns a new reader of the contents of this data value.
+     */
+    public BerDataValueReader contentsReader() {
+        return new ByteBufferBerDataValueReader(getEncodedContents());
+    }
+
+    /**
+     * Returns a new reader which returns just this data value. This may be useful for re-reading
+     * this value in different contexts.
+     */
+    public BerDataValueReader dataValueReader() {
+        return new ParsedValueReader(this);
+    }
+
+    private static final class ParsedValueReader implements BerDataValueReader {
+        private final BerDataValue mValue;
+        private boolean mValueOutput;
+
+        public ParsedValueReader(BerDataValue value) {
+            mValue = value;
+        }
+
+        @Override
+        public BerDataValue readDataValue() throws BerDataValueFormatException {
+            if (mValueOutput) {
+                return null;
+            }
+            mValueOutput = true;
+            return mValue;
+        }
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java
new file mode 100644
index 0000000..11ef6c3
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+/**
+ * Indicates that an ASN.1 data value being read could not be decoded using
+ * Basic Encoding Rules (BER).
+ */
+public class BerDataValueFormatException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public BerDataValueFormatException(String message) {
+        super(message);
+    }
+
+    public BerDataValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java
new file mode 100644
index 0000000..eb2f383
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+/**
+ * Reader of ASN.1 Basic Encoding Rules (BER) data values.
+ *
+ * <p>BER data value reader returns data values, one by one, from a source. The interpretation of
+ * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
+ * the elements of a SEQUENCE value) is left to clients of the reader.
+ */
+public interface BerDataValueReader {
+
+    /**
+     * Returns the next data value or {@code null} if end of input has been reached.
+     *
+     * @throws BerDataValueFormatExcepton if the value being read is malformed.
+     */
+    BerDataValue readDataValue() throws BerDataValueFormatException;
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
new file mode 100644
index 0000000..f893a65
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
+ */
+public abstract class BerEncoding {
+    private BerEncoding() {}
+
+    /**
+     * Constructed vs primitive flag in the first identifier byte.
+     */
+    public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
+
+    /**
+     * Tag class: UNIVERSAL
+     */
+    public static final int TAG_CLASS_UNIVERSAL = 0;
+
+    /**
+     * Tag class: APPLICATION
+     */
+    public static final int TAG_CLASS_APPLICATION = 1;
+
+    /**
+     * Tag class: CONTEXT SPECIFIC
+     */
+    public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
+
+    /**
+     * Tag class: PRIVATE
+     */
+    public static final int TAG_CLASS_PRIVATE = 3;
+
+    /**
+     * Tag number: INTEGER
+     */
+    public static final int TAG_NUMBER_INTEGER = 0x2;
+
+    /**
+     * Tag number: OCTET STRING
+     */
+    public static final int TAG_NUMBER_OCTET_STRING = 0x4;
+
+    /**
+     * Tag number: NULL
+     */
+    public static final int TAG_NUMBER_NULL = 0x05;
+
+    /**
+     * Tag number: OBJECT IDENTIFIER
+     */
+    public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
+
+    /**
+     * Tag number: SEQUENCE
+     */
+    public static final int TAG_NUMBER_SEQUENCE = 0x10;
+
+    /**
+     * Tag number: SET
+     */
+    public static final int TAG_NUMBER_SET = 0x11;
+
+    public static String tagNumberToString(int tagNumber) {
+        switch (tagNumber) {
+            case TAG_NUMBER_INTEGER:
+                return "INTEGER";
+            case TAG_NUMBER_OCTET_STRING:
+                return "OCTET STRING";
+            case TAG_NUMBER_NULL:
+                return "NULL";
+            case TAG_NUMBER_OBJECT_IDENTIFIER:
+                return "OBJECT IDENTIFIER";
+            case TAG_NUMBER_SEQUENCE:
+                return "SEQUENCE";
+            case TAG_NUMBER_SET:
+                return "SET";
+            default:
+                return "0x" + Integer.toHexString(tagNumber);
+        }
+    }
+
+    /**
+     * Returns {@code true} if the provided first identifier byte indicates that the data value uses
+     * constructed encoding for its contents, or {@code false} if the data value uses primitive
+     * encoding for its contents.
+     */
+    public static boolean isConstructed(byte firstIdentifierByte) {
+        return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
+    }
+
+    /**
+     * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
+     * constants.
+     */
+    public static int getTagClass(byte firstIdentifierByte) {
+        return (firstIdentifierByte & 0xff) >> 6;
+    }
+
+    /**
+     * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
+     * constants.
+     */
+    public static int getTagNumber(byte firstIdentifierByte) {
+        return firstIdentifierByte & 0x1f;
+    }
+}
+
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java b/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java
new file mode 100644
index 0000000..adf2a25
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class ByteBufferBerDataValueReader implements BerDataValueReader {
+    private final ByteBuffer mBuf;
+
+    public ByteBufferBerDataValueReader(ByteBuffer buf) {
+        if (buf == null) {
+            throw new NullPointerException("buf == null");
+        }
+        mBuf = buf;
+    }
+
+    @Override
+    public BerDataValue readDataValue() throws BerDataValueFormatException {
+        int startPosition = mBuf.position();
+        if (!mBuf.hasRemaining()) {
+            return null;
+        }
+        byte firstIdentifierByte = mBuf.get();
+        int tagNumber = readTagNumber(firstIdentifierByte);
+
+        if (!mBuf.hasRemaining()) {
+            throw new BerDataValueFormatException("Missing length");
+        }
+        int firstLengthByte = mBuf.get() & 0xff;
+        int contentsLength;
+        int contentsOffsetInTag;
+        if ((firstLengthByte & 0x80) == 0) {
+            // short form length
+            contentsLength = readShortFormLength(firstLengthByte);
+            contentsOffsetInTag = mBuf.position() - startPosition;
+            skipDefiniteLengthContents(contentsLength);
+        } else if (firstLengthByte != 0x80) {
+            // long form length
+            contentsLength = readLongFormLength(firstLengthByte);
+            contentsOffsetInTag = mBuf.position() - startPosition;
+            skipDefiniteLengthContents(contentsLength);
+        } else {
+            // indefinite length -- value ends with 0x00 0x00
+            contentsOffsetInTag = mBuf.position() - startPosition;
+            contentsLength = skipIndefiniteLengthContents();
+        }
+
+        // Create the encoded data value ByteBuffer
+        int endPosition = mBuf.position();
+        mBuf.position(startPosition);
+        int bufOriginalLimit = mBuf.limit();
+        mBuf.limit(endPosition);
+        ByteBuffer encoded = mBuf.slice();
+        mBuf.position(mBuf.limit());
+        mBuf.limit(bufOriginalLimit);
+
+        // Create the encoded contents ByteBuffer
+        encoded.position(contentsOffsetInTag);
+        encoded.limit(contentsOffsetInTag + contentsLength);
+        ByteBuffer encodedContents = encoded.slice();
+        encoded.clear();
+
+        return new BerDataValue(
+                encoded,
+                encodedContents,
+                BerEncoding.getTagClass(firstIdentifierByte),
+                BerEncoding.isConstructed(firstIdentifierByte),
+                tagNumber);
+    }
+
+    private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
+        int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
+        if (tagNumber == 0x1f) {
+            // high-tag-number form, where the tag number follows this byte in base-128
+            // big-endian form, where each byte has the highest bit set, except for the last
+            // byte
+            return readHighTagNumber();
+        } else {
+            // low-tag-number form
+            return tagNumber;
+        }
+    }
+
+    private int readHighTagNumber() throws BerDataValueFormatException {
+        // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+        // byte
+        int b;
+        int result = 0;
+        do {
+            if (!mBuf.hasRemaining()) {
+                throw new BerDataValueFormatException("Truncated tag number");
+            }
+            b = mBuf.get();
+            if (result > Integer.MAX_VALUE >>> 7) {
+                throw new BerDataValueFormatException("Tag number too large");
+            }
+            result <<= 7;
+            result |= b & 0x7f;
+        } while ((b & 0x80) != 0);
+        return result;
+    }
+
+    private int readShortFormLength(int firstLengthByte) throws BerDataValueFormatException {
+        return firstLengthByte & 0x7f;
+    }
+
+    private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
+        // The low 7 bits of the first byte represent the number of bytes (following the first
+        // byte) in which the length is in big-endian base-256 form
+        int byteCount = firstLengthByte & 0x7f;
+        if (byteCount > 4) {
+            throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+        }
+        int result = 0;
+        for (int i = 0; i < byteCount; i++) {
+            if (!mBuf.hasRemaining()) {
+                throw new BerDataValueFormatException("Truncated length");
+            }
+            int b = mBuf.get();
+            if (result > Integer.MAX_VALUE >>> 8) {
+                throw new BerDataValueFormatException("Length too large");
+            }
+            result <<= 8;
+            result |= b & 0xff;
+        }
+        return result;
+    }
+
+    private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
+        if (mBuf.remaining() < contentsLength) {
+            throw new BerDataValueFormatException(
+                    "Truncated contents. Need: " + contentsLength + " bytes, available: "
+                            + mBuf.remaining());
+        }
+        mBuf.position(mBuf.position() + contentsLength);
+    }
+
+    private int skipIndefiniteLengthContents() throws BerDataValueFormatException {
+        // Contents are terminated by 0x00 0x00
+        boolean prevZeroByte = false;
+        int bytesRead = 0;
+        while (true) {
+            if (!mBuf.hasRemaining()) {
+                throw new BerDataValueFormatException(
+                        "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+
+            }
+            int b = mBuf.get();
+            bytesRead++;
+            if (bytesRead < 0) {
+                throw new BerDataValueFormatException("Indefinite-length contents too long");
+            }
+            if (b == 0) {
+                if (prevZeroByte) {
+                    // End of contents reached -- we've read the value and its terminator 0x00 0x00
+                    return bytesRead - 2;
+                }
+                prevZeroByte = true;
+            } else {
+                prevZeroByte = false;
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java b/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java
new file mode 100644
index 0000000..9dfec15
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class InputStreamBerDataValueReader implements BerDataValueReader {
+    private final InputStream mIn;
+
+    public InputStreamBerDataValueReader(InputStream in) {
+        if (in == null) {
+            throw new NullPointerException("in == null");
+        }
+        mIn = in;
+    }
+
+    @SuppressWarnings("resource")
+    @Override
+    public BerDataValue readDataValue() throws BerDataValueFormatException {
+        RecordingInputStream in = new RecordingInputStream(mIn);
+
+        try {
+            int firstIdentifierByte = in.read();
+            if (firstIdentifierByte == -1) {
+                // End of input
+                return null;
+            }
+            int tagNumber = readTagNumber(in, firstIdentifierByte);
+
+            int firstLengthByte = in.read();
+            if (firstLengthByte == -1) {
+                throw new BerDataValueFormatException("Missing length");
+            }
+
+            int contentsLength;
+            int contentsOffsetInDataValue;
+            if ((firstLengthByte & 0x80) == 0) {
+                // short form length
+                contentsLength = readShortFormLength(firstLengthByte);
+                contentsOffsetInDataValue = in.getReadByteCount();
+                skipDefiniteLengthContents(in, contentsLength);
+            } else if ((firstLengthByte & 0xff) != 0x80) {
+                // long form length
+                contentsLength = readLongFormLength(in, firstLengthByte);
+                contentsOffsetInDataValue = in.getReadByteCount();
+                skipDefiniteLengthContents(in, contentsLength);
+            } else {
+                // indefinite length
+                contentsOffsetInDataValue = in.getReadByteCount();
+                contentsLength = skipIndefiniteLengthContents(in);
+            }
+
+            byte[] encoded = in.getReadBytes();
+            ByteBuffer encodedContents =
+                    ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
+            return new BerDataValue(
+                    ByteBuffer.wrap(encoded),
+                    encodedContents,
+                    BerEncoding.getTagClass((byte) firstIdentifierByte),
+                    BerEncoding.isConstructed((byte) firstIdentifierByte),
+                    tagNumber);
+        } catch (IOException e) {
+            throw new BerDataValueFormatException("Failed to read data value", e);
+        }
+    }
+
+    private static int readTagNumber(InputStream in, int firstIdentifierByte)
+            throws IOException, BerDataValueFormatException {
+        int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
+        if (tagNumber == 0x1f) {
+            // high-tag-number form
+            return readHighTagNumber(in);
+        } else {
+            // low-tag-number form
+            return tagNumber;
+        }
+    }
+
+    private static int readHighTagNumber(InputStream in)
+            throws IOException, BerDataValueFormatException {
+        // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+        // byte where the highest bit is not set
+        int b;
+        int result = 0;
+        do {
+            b = in.read();
+            if (b == -1) {
+                throw new BerDataValueFormatException("Truncated tag number");
+            }
+            if (result > Integer.MAX_VALUE >>> 7) {
+                throw new BerDataValueFormatException("Tag number too large");
+            }
+            result <<= 7;
+            result |= b & 0x7f;
+        } while ((b & 0x80) != 0);
+        return result;
+    }
+
+    private static int readShortFormLength(int firstLengthByte) throws BerDataValueFormatException {
+        return firstLengthByte & 0x7f;
+    }
+
+    private static int readLongFormLength(InputStream in, int firstLengthByte)
+            throws IOException, BerDataValueFormatException {
+        // The low 7 bits of the first byte represent the number of bytes (following the first
+        // byte) in which the length is in big-endian base-256 form
+        int byteCount = firstLengthByte & 0x7f;
+        if (byteCount > 4) {
+            throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+        }
+        int result = 0;
+        for (int i = 0; i < byteCount; i++) {
+            int b = in.read();
+            if (b == -1) {
+                throw new BerDataValueFormatException("Truncated length");
+            }
+            if (result > Integer.MAX_VALUE >>> 8) {
+                throw new BerDataValueFormatException("Length too large");
+            }
+            result <<= 8;
+            result |= b & 0xff;
+        }
+        return result;
+    }
+
+    private static void skipDefiniteLengthContents(InputStream in, int len)
+            throws IOException, BerDataValueFormatException {
+        long bytesRead = 0;
+        while (len > 0) {
+            int skipped = (int) in.skip(len);
+            if (skipped <= 0) {
+                throw new BerDataValueFormatException(
+                        "Truncated definite-length contents: " + bytesRead + " bytes read"
+                                + ", " + len + " missing");
+            }
+            len -= skipped;
+            bytesRead += skipped;
+        }
+    }
+
+    private static int skipIndefiniteLengthContents(InputStream in)
+            throws IOException, BerDataValueFormatException {
+        // Contents are terminated by 0x00 0x00
+        boolean prevZeroByte = false;
+        int bytesRead = 0;
+        while (true) {
+            int b = in.read();
+            if (b == -1) {
+                throw new BerDataValueFormatException(
+                        "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+            }
+            bytesRead++;
+            if (bytesRead < 0) {
+                throw new BerDataValueFormatException("Indefinite-length contents too long");
+            }
+            if (b == 0) {
+                if (prevZeroByte) {
+                    // End of contents reached -- we've read the value and its terminator 0x00 0x00
+                    return bytesRead - 2;
+                }
+                prevZeroByte = true;
+                continue;
+            } else {
+                prevZeroByte = false;
+            }
+        }
+    }
+
+    private static class RecordingInputStream extends InputStream {
+        private final InputStream mIn;
+        private final ByteArrayOutputStream mBuf;
+
+        private RecordingInputStream(InputStream in) {
+            mIn = in;
+            mBuf = new ByteArrayOutputStream();
+        }
+
+        public byte[] getReadBytes() {
+            return mBuf.toByteArray();
+        }
+
+        public int getReadByteCount() {
+            return mBuf.size();
+        }
+
+        @Override
+        public int read() throws IOException {
+            int b = mIn.read();
+            if (b != -1) {
+                mBuf.write(b);
+            }
+            return b;
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            int len = mIn.read(b);
+            if (len > 0) {
+                mBuf.write(b, 0, len);
+            }
+            return len;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            len = mIn.read(b, off, len);
+            if (len > 0) {
+                mBuf.write(b, off, len);
+            }
+            return len;
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            if (n <= 0) {
+                return mIn.skip(n);
+            }
+
+            byte[] buf = new byte[4096];
+            int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
+            if (len > 0) {
+                mBuf.write(buf, 0, len);
+            }
+            return (len < 0) ? 0 : len;
+        }
+
+        @Override
+        public int available() throws IOException {
+            return super.available();
+        }
+
+        @Override
+        public void close() throws IOException {
+            super.close();
+        }
+
+        @Override
+        public synchronized void mark(int readlimit) {}
+
+        @Override
+        public synchronized void reset() throws IOException {
+            throw new IOException("mark/reset not supported");
+        }
+
+        @Override
+        public boolean markSupported() {
+            return false;
+        }
+    }
+}
diff --git a/src/test/java/com/android/apksig/AllTests.java b/src/test/java/com/android/apksig/AllTests.java
new file mode 100644
index 0000000..a9f3f5e
--- /dev/null
+++ b/src/test/java/com/android/apksig/AllTests.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    com.android.apksig.apk.AllTests.class,
+    com.android.apksig.internal.AllTests.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/apk/AllTests.java b/src/test/java/com/android/apksig/apk/AllTests.java
new file mode 100644
index 0000000..64895ab
--- /dev/null
+++ b/src/test/java/com/android/apksig/apk/AllTests.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    ApkUtilsTest.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/AllTests.java b/src/test/java/com/android/apksig/internal/AllTests.java
new file mode 100644
index 0000000..d83df58
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/AllTests.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    com.android.apksig.internal.asn1.AllTests.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/asn1/AllTests.java b/src/test/java/com/android/apksig/internal/asn1/AllTests.java
new file mode 100644
index 0000000..9f7265a
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/AllTests.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    com.android.apksig.internal.asn1.ber.AllTests.class
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/AllTests.java b/src/test/java/com/android/apksig/internal/asn1/ber/AllTests.java
new file mode 100644
index 0000000..6916164
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/AllTests.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    BerDataValueTest.class,
+    ByteBufferBerDataValueReaderTest.class,
+    InputStreamBerDataValueReaderTest.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueReaderTestBase.java b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueReaderTestBase.java
new file mode 100644
index 0000000..c74a74f
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueReaderTestBase.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import static com.android.apksig.internal.test.MoreAsserts.assertByteBufferEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.android.apksig.internal.test.HexEncoding;
+
+/**
+ * Base class for unit tests of ASN.1 BER (see {@code X.690}) data value reader implementations.
+ *
+ * <p>Subclasses need to provide only an implementation of {@link #createReader(byte[])} and
+ * subclass-specific tests.
+ */
+public abstract class BerDataValueReaderTestBase {
+
+    /**
+     * Returns a new reader initialized with the provided input.
+     */
+    protected abstract BerDataValueReader createReader(byte[] input);
+
+    @Test
+    public void testEmptyInput() throws Exception {
+        assertNull(readDataValue(""));
+    }
+
+    @Test
+    public void testEndOfInput() throws Exception {
+        BerDataValueReader reader = createReader("3000"); // SEQUENCE with empty contents
+        assertNotNull(reader.readDataValue());
+        // End of input has been reached
+        assertNull(reader.readDataValue());
+        // Null should also be returned on consecutive invocations
+        assertNull(reader.readDataValue());
+    }
+
+    @Test
+    public void testSingleByteTagId() throws Exception {
+        BerDataValue dataValue = readDataValue("1000");
+        assertEquals(BerEncoding.TAG_CLASS_UNIVERSAL, dataValue.getTagClass());
+        assertFalse(dataValue.isConstructed());
+        assertEquals(0x10, dataValue.getTagNumber());
+
+        dataValue = readDataValue("3900");
+        assertEquals(BerEncoding.TAG_CLASS_UNIVERSAL, dataValue.getTagClass());
+        assertTrue(dataValue.isConstructed());
+        assertEquals(0x19, dataValue.getTagNumber());
+
+        dataValue = readDataValue("6700");
+        assertEquals(BerEncoding.TAG_CLASS_APPLICATION, dataValue.getTagClass());
+        assertTrue(dataValue.isConstructed());
+        assertEquals(7, dataValue.getTagNumber());
+
+        dataValue = readDataValue("8600");
+        assertEquals(BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC, dataValue.getTagClass());
+        assertFalse(dataValue.isConstructed());
+        assertEquals(6, dataValue.getTagNumber());
+
+        dataValue = readDataValue("fe00");
+        assertEquals(BerEncoding.TAG_CLASS_PRIVATE, dataValue.getTagClass());
+        assertTrue(dataValue.isConstructed());
+        assertEquals(0x1e, dataValue.getTagNumber());
+    }
+
+    @Test
+    public void testHighTagNumber() throws Exception {
+        assertEquals(7, readDataValue("3f0700").getTagNumber());
+        assertEquals(7, readDataValue("3f800700").getTagNumber());
+        assertEquals(7, readDataValue("3f80800700").getTagNumber());
+        assertEquals(7, readDataValue("3f8080800700").getTagNumber());
+        assertEquals(7, readDataValue("3f808080808080808080808080808080800700").getTagNumber());
+        assertEquals(375, readDataValue("3f827700").getTagNumber());
+        assertEquals(268435455, readDataValue("3fffffff7f00").getTagNumber());
+        assertEquals(Integer.MAX_VALUE, readDataValue("3f87ffffff7f00").getTagNumber());
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testHighTagNumberTooLarge() throws Exception {
+        readDataValue("3f888080800000"); // Integer.MAX_VALUE + 1
+    }
+
+    // @Test(expected = BerDataValueFormatException.class)
+    public void testTruncatedHighTagNumberLastOctetMissing() throws Exception {
+        readDataValue("9f80"); // terminating octet must not have the highest bit set
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testTruncatedBeforeFirstLengthOctet() throws Exception {
+        readDataValue("30");
+    }
+
+    @Test
+    public void testShortFormLength() throws Exception {
+        assertByteBufferEquals(new byte[0], readDataValue("3000").getEncodedContents());
+        assertByteBufferEquals(
+                HexEncoding.decode("010203"), readDataValue("3003010203").getEncodedContents());
+    }
+
+    @Test
+    public void testLongFormLength() throws Exception {
+        assertByteBufferEquals(new byte[0], readDataValue("308100").getEncodedContents());
+        assertByteBufferEquals(
+                HexEncoding.decode("010203"), readDataValue("30820003010203").getEncodedContents());
+        assertEquals(
+                255,
+                readDataValue(concat(HexEncoding.decode("3081ff"), new byte[255]))
+                        .getEncodedContents().remaining());
+        assertEquals(
+                0x110,
+                readDataValue(concat(HexEncoding.decode("30820110"), new byte[0x110]))
+                        .getEncodedContents().remaining());
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testTruncatedLongFormLengthBeforeFirstLengthByte() throws Exception {
+        readDataValue("3081");
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testTruncatedLongFormLengthLastLengthByteMissing() throws Exception {
+        readDataValue("308200");
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testLongFormLengthTooLarge() throws Exception {
+        readDataValue("3084ffffffff");
+    }
+
+    @Test
+    public void testIndefiniteFormLength() throws Exception {
+        assertByteBufferEquals(new byte[0], readDataValue("30800000").getEncodedContents());
+        assertByteBufferEquals(
+                HexEncoding.decode("010203"), readDataValue("30800102030000").getEncodedContents());
+        assertByteBufferEquals(
+                HexEncoding.decode(
+                        "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"
+                            + "000102030405060708090a0b0c0d0e0f"),
+                readDataValue(
+                        "3080"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "000102030405060708090a0b0c0d0e0f"
+                                + "0000"
+                        ).getEncodedContents());
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testDefiniteLengthContentsTruncatedBeforeFirstContentOctet() throws Exception {
+        readDataValue("3001");
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testIndefiniteLengthContentsTruncatedBeforeFirstContentOctet() throws Exception {
+        readDataValue("3080");
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testTruncatedDefiniteLengthContents() throws Exception {
+        readDataValue("30030102");
+    }
+
+    @Test(expected = BerDataValueFormatException.class)
+    public void testTruncatedIndefiniteLengthContents() throws Exception {
+        readDataValue("308001020300");
+    }
+
+    @Test
+    public void testEmptyDefiniteLengthContents() throws Exception {
+        assertByteBufferEquals(new byte[0], readDataValue("3000").getEncodedContents());
+    }
+
+    @Test
+    public void testEmptyIndefiniteLengthContents() throws Exception {
+        assertByteBufferEquals(new byte[0], readDataValue("30800000").getEncodedContents());
+    }
+
+    @Test
+    public void testReadAdvancesPosition() throws Exception {
+        BerDataValueReader reader = createReader("37018f050001020304");
+        assertByteBufferEquals(HexEncoding.decode("37018f"), reader.readDataValue().getEncoded());
+        assertByteBufferEquals(HexEncoding.decode("0500"), reader.readDataValue().getEncoded());
+        assertByteBufferEquals(HexEncoding.decode("01020304"), reader.readDataValue().getEncoded());
+        assertNull(reader.readDataValue());
+    }
+
+    private BerDataValueReader createReader(String hexEncodedInput) {
+        return createReader(HexEncoding.decode(hexEncodedInput));
+    }
+
+    private BerDataValue readDataValue(byte[] input)
+            throws BerDataValueFormatException {
+        return createReader(input).readDataValue();
+    }
+
+    private BerDataValue readDataValue(String hexEncodedInput)
+            throws BerDataValueFormatException {
+        return createReader(hexEncodedInput).readDataValue();
+    }
+
+    private static byte[] concat(byte[] arr1, byte[] arr2) {
+        byte[] result = new byte[arr1.length + arr2.length];
+        System.arraycopy(arr1,  0, result, 0, arr1.length);
+        System.arraycopy(arr2,  0, result, arr1.length, arr2.length);
+        return result;
+    }
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueTest.java b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueTest.java
new file mode 100644
index 0000000..9f40e1e
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import static com.android.apksig.internal.test.MoreAsserts.assertByteBufferEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.ByteBuffer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.android.apksig.internal.test.HexEncoding;
+
+@RunWith(JUnit4.class)
+public class BerDataValueTest {
+    private static final BerDataValue TEST_VALUE1 =
+            new BerDataValue(
+                    ByteBuffer.wrap(HexEncoding.decode("aa")),
+                    ByteBuffer.wrap(HexEncoding.decode("bb")),
+                    BerEncoding.TAG_CLASS_UNIVERSAL,
+                    true,
+                    BerEncoding.TAG_NUMBER_SEQUENCE);
+
+    private static final BerDataValue TEST_VALUE2 =
+            new BerDataValue(
+                    ByteBuffer.wrap(HexEncoding.decode("cc")),
+                    ByteBuffer.wrap(HexEncoding.decode("dd")),
+                    BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC,
+                    false,
+                    BerEncoding.TAG_NUMBER_OCTET_STRING);
+
+    @Test
+    public void testGetTagClass() {
+        assertEquals(BerEncoding.TAG_CLASS_UNIVERSAL, TEST_VALUE1.getTagClass());
+        assertEquals(BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC, TEST_VALUE2.getTagClass());
+    }
+
+    @Test
+    public void testIsConstructed() {
+        assertTrue(TEST_VALUE1.isConstructed());
+        assertFalse(TEST_VALUE2.isConstructed());
+    }
+
+    @Test
+    public void testGetTagNumber() {
+        assertEquals(BerEncoding.TAG_NUMBER_SEQUENCE, TEST_VALUE1.getTagNumber());
+        assertEquals(BerEncoding.TAG_NUMBER_OCTET_STRING, TEST_VALUE2.getTagNumber());
+    }
+
+    @Test
+    public void testGetEncoded() {
+        assertByteBufferEquals(HexEncoding.decode("aa"), TEST_VALUE1.getEncoded());
+        assertByteBufferEquals(HexEncoding.decode("cc"), TEST_VALUE2.getEncoded());
+    }
+
+    @Test
+    public void testGetEncodedReturnsSlice() {
+        // Assert that changing the position of returned ByteBuffer does not affect ByteBuffers
+        // returned in the future
+        ByteBuffer encoded = TEST_VALUE1.getEncoded();
+        assertByteBufferEquals(HexEncoding.decode("aa"), encoded);
+        encoded.position(encoded.limit());
+        assertByteBufferEquals(HexEncoding.decode("aa"), TEST_VALUE1.getEncoded());
+    }
+
+    @Test
+    public void testGetEncodedContents() {
+        assertByteBufferEquals(HexEncoding.decode("bb"), TEST_VALUE1.getEncodedContents());
+        assertByteBufferEquals(HexEncoding.decode("dd"), TEST_VALUE2.getEncodedContents());
+    }
+
+    @Test
+    public void testGetEncodedContentsReturnsSlice() {
+        // Assert that changing the position of returned ByteBuffer does not affect ByteBuffers
+        // returned in the future
+        ByteBuffer encoded = TEST_VALUE1.getEncodedContents();
+        assertByteBufferEquals(HexEncoding.decode("bb"), encoded);
+        encoded.position(encoded.limit());
+        assertByteBufferEquals(HexEncoding.decode("bb"), TEST_VALUE1.getEncodedContents());
+    }
+
+    @Test
+    public void testDataValueReader() throws BerDataValueFormatException {
+        BerDataValueReader reader = TEST_VALUE1.dataValueReader();
+        assertSame(TEST_VALUE1, reader.readDataValue());
+        assertNull(reader.readDataValue());
+        assertNull(reader.readDataValue());
+    }
+
+    @Test
+    public void testContentsReader() throws BerDataValueFormatException {
+        BerDataValue dataValue =
+                new BerDataValue(
+                        ByteBuffer.allocate(0),
+                        ByteBuffer.wrap(HexEncoding.decode("300203040500")),
+                        BerEncoding.TAG_CLASS_UNIVERSAL,
+                        true,
+                        BerEncoding.TAG_NUMBER_SEQUENCE);
+        BerDataValueReader reader = dataValue.contentsReader();
+        assertEquals(ByteBufferBerDataValueReader.class, reader.getClass());
+        assertByteBufferEquals(HexEncoding.decode("30020304"), reader.readDataValue().getEncoded());
+        assertByteBufferEquals(HexEncoding.decode("0500"), reader.readDataValue().getEncoded());
+        assertNull(reader.readDataValue());
+    }
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReaderTest.java b/src/test/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReaderTest.java
new file mode 100644
index 0000000..8875e5c
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReaderTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ByteBufferBerDataValueReaderTest extends BerDataValueReaderTestBase {
+
+    @Override
+    protected ByteBufferBerDataValueReader createReader(byte[] input) {
+        return new ByteBufferBerDataValueReader(ByteBuffer.wrap(input));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructWithNullByteBuffer() throws Exception {
+        new ByteBufferBerDataValueReader(null);
+    }
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReaderTest.java b/src/test/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReaderTest.java
new file mode 100644
index 0000000..4c4086b
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReaderTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.io.ByteArrayInputStream;
+
+import org.junit.Test;
+
+public class InputStreamBerDataValueReaderTest extends BerDataValueReaderTestBase {
+
+    @Override
+    protected InputStreamBerDataValueReader createReader(byte[] input) {
+        return new InputStreamBerDataValueReader(new ByteArrayInputStream(input));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructWithNullByteBuffer() throws Exception {
+        new InputStreamBerDataValueReader(null);
+    }
+}
diff --git a/src/test/java/com/android/apksig/internal/test/HexEncoding.java b/src/test/java/com/android/apksig/internal/test/HexEncoding.java
new file mode 100644
index 0000000..cfd2590
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/test/HexEncoding.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.test;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Hexadecimal encoding where each byte is represented by two hexadecimal digits.
+ *
+ * @hide
+ */
+public class HexEncoding {
+
+    /** Hidden constructor to prevent instantiation. */
+    private HexEncoding() {}
+
+    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+
+    /**
+     * Encodes the provided data as a hexadecimal string.
+     */
+    public static String encode(byte[] data) {
+        return encode(data, 0, data.length);
+    }
+
+    /**
+     * Encodes the provided data as a hexadecimal string.
+     */
+    public static String encode(byte[] data, int offset, int len) {
+      StringBuilder result = new StringBuilder(len * 2);
+      for (int i = 0; i < len; i++) {
+          byte b = data[offset + i];
+          result.append(HEX_DIGITS[(b >>> 4) & 0x0f]);
+          result.append(HEX_DIGITS[b & 0x0f]);
+      }
+      return result.toString();
+    }
+
+    /**
+     * Encodes the provided data as a hexadecimal string.
+     */
+    public static String encode(ByteBuffer buf) {
+        return encode(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+    }
+
+    /**
+     * Decodes the provided hexadecimal string into an array of bytes.
+     */
+    public static byte[] decode(String encoded) {
+        // IMPLEMENTATION NOTE: Special care is taken to permit odd number of hexadecimal digits.
+        int resultLengthBytes = (encoded.length() + 1) / 2;
+        byte[] result = new byte[resultLengthBytes];
+        int resultOffset = 0;
+        int encodedCharOffset = 0;
+        if ((encoded.length() % 2) != 0) {
+            // Odd number of digits -- the first digit is the lower 4 bits of the first result byte.
+            result[resultOffset++] =
+                    (byte) getHexadecimalDigitValue(encoded.charAt(encodedCharOffset));
+            encodedCharOffset++;
+        }
+        for (int len = encoded.length(); encodedCharOffset < len; encodedCharOffset += 2) {
+          result[resultOffset++] = (byte)
+                  ((getHexadecimalDigitValue(encoded.charAt(encodedCharOffset)) << 4)
+                          | getHexadecimalDigitValue(encoded.charAt(encodedCharOffset + 1)));
+        }
+        return result;
+    }
+
+    private static int getHexadecimalDigitValue(char c) {
+        if ((c >= 'a') && (c <= 'f')) {
+            return (c - 'a') + 0x0a;
+        } else if ((c >= 'A') && (c <= 'F')) {
+            return (c - 'A') + 0x0a;
+        } else if ((c >= '0') && (c <= '9')) {
+            return c - '0';
+        } else {
+            throw new IllegalArgumentException(
+                    "Invalid hexadecimal digit at position : '"
+                            + c + "' (0x" + Integer.toHexString(c) + ")");
+        }
+    }
+}
diff --git a/src/test/java/com/android/apksig/internal/test/MoreAsserts.java b/src/test/java/com/android/apksig/internal/test/MoreAsserts.java
new file mode 100644
index 0000000..409e95e
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/test/MoreAsserts.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.test;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.nio.ByteBuffer;
+
+public abstract class MoreAsserts {
+    private MoreAsserts() {}
+
+    /**
+     * Asserts that the contents of the provided {@code ByteBuffer} are as expected. This method
+     * does not change the position or the limit of the provided buffer.
+     */
+    public static void assertByteBufferEquals(byte[] expected, ByteBuffer actual) {
+        assertByteBufferEquals(null, expected, actual);
+    }
+
+    /**
+     * Asserts that the contents of the provided {@code ByteBuffer} are as expected. This method
+     * does not change the position or the limit of the provided buffer.
+     */
+    public static void assertByteBufferEquals(String message, byte[] expected, ByteBuffer actual) {
+        byte[] actualArr;
+        if ((actual.hasArray())
+                && (actual.arrayOffset() == 0) && (actual.array().length == actual.remaining())) {
+            actualArr = actual.array();
+        } else {
+            actualArr = new byte[actual.remaining()];
+            int actualOriginalPos = actual.position();
+            actual.get(actualArr);
+            actual.position(actualOriginalPos);
+        }
+        assertArrayEquals(message, expected, actualArr);
+    }
+}