Resolve 'redundant length bytes' error when parsing BER encoded certificate

Change-Id: Ib1e8726818010862c3a7df5999c127ef21e0bbfd
Fixes: 67513776
Test: gradlew build and manual verification of failing APK
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index aeb02cd..c91d219 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -22,6 +22,7 @@
 import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 
@@ -1347,7 +1348,7 @@
             // Load certificates
             Collection<? extends Certificate> certs;
             try (FileInputStream in = new FileInputStream(certFile)) {
-                certs = CertificateFactory.getInstance("X.509").generateCertificates(in);
+                certs = X509CertificateUtils.generateCertificates(in);
             }
             List<X509Certificate> certList = new ArrayList<>(certs.size());
             for (Certificate cert : certs) {
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
index a828bcc..2fd7808 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -37,6 +37,7 @@
 import com.android.apksig.internal.pkcs7.SignerInfo;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
 import com.android.apksig.internal.util.InclusiveIntRange;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -44,7 +45,7 @@
 import com.android.apksig.util.DataSinks;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.zip.ZipFormatException;
-import java.io.ByteArrayInputStream;
+
 import java.io.IOException;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
@@ -56,7 +57,6 @@
 import java.security.Signature;
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -72,6 +72,7 @@
 import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.jar.Attributes;
+
 import javax.security.auth.x500.X500Principal;
 
 /**
@@ -748,22 +749,13 @@
                 return Collections.emptyList();
             }
 
-            CertificateFactory certFactory;
-            try {
-                certFactory = CertificateFactory.getInstance("X.509");
-            } catch (CertificateException e) {
-                throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
-            }
-
             List<X509Certificate> result = new ArrayList<>(encodedCertificates.size());
             for (int i = 0; i < encodedCertificates.size(); i++) {
                 Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
                 X509Certificate certificate;
                 byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded());
                 try {
-                    certificate =
-                            (X509Certificate) certFactory.generateCertificate(
-                                    new ByteArrayInputStream(encodedForm));
+                    certificate = X509CertificateUtils.generateCertificate(encodedForm);
                 } catch (CertificateException e) {
                     throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
                 }
@@ -848,6 +840,8 @@
         private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
         private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
         static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
+        static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
+        static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
 
         static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
         private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
@@ -1215,6 +1209,8 @@
                 OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
                 OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
                 OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
 
                 OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
                 OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
index a7c5d8f..bbef027 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -24,6 +24,7 @@
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
 import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
 import com.android.apksig.util.DataSource;
 
@@ -345,10 +346,7 @@
             byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
             X509Certificate certificate;
             try {
-                certificate =
-                        (X509Certificate)
-                                certFactory.generateCertificate(
-                                        new ByteArrayInputStream(encodedCert));
+                certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
             } catch (CertificateException e) {
                 result.addError(
                         Issue.V2_SIG_MALFORMED_CERTIFICATE,
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index 453f12c..9a2932b 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -30,10 +30,10 @@
 import com.android.apksig.internal.apk.SignatureInfo;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
 import com.android.apksig.util.DataSource;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -293,7 +293,7 @@
         int parsedMaxSdkVersion = signerBlock.getInt();
         result.minSdkVersion = parsedMinSdkVersion;
         result.maxSdkVersion = parsedMaxSdkVersion;
-        if (parsedMinSdkVersion < 1 || parsedMinSdkVersion > parsedMaxSdkVersion) {
+        if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
             result.addError(
                     Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
         }
@@ -407,10 +407,7 @@
             byte[] encodedCert = readLengthPrefixedByteArray(certificates);
             X509Certificate certificate;
             try {
-                certificate =
-                        (X509Certificate)
-                                certFactory.generateCertificate(
-                                        new ByteArrayInputStream(encodedCert));
+                certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
             } catch (CertificateException e) {
                 result.addError(
                         Issue.V3_SIG_MALFORMED_CERTIFICATE,
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
index 946b03a..e1e01a9 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
@@ -25,8 +25,8 @@
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -39,7 +39,6 @@
 import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.AlgorithmParameterSpec;
 import java.util.ArrayList;
@@ -93,12 +92,6 @@
         //   * uint32: signature algorithm id (used by to sign next cert in lineage)
         //   * length-prefixed bytes: signature over above signed data
 
-        CertificateFactory certFactory;
-        try {
-            certFactory = CertificateFactory.getInstance("X.509");
-        } catch (CertificateException e) {
-            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
-        }
         X509Certificate lastCert = null;
         int lastSigAlgorithmId = 0;
 
@@ -146,8 +139,7 @@
                     throw new SecurityException("Signing algorithm ID mismatch for certificate #"
                             + nodeBytes + " when verifying V3SigningCertificateLineage object");
                 }
-                lastCert = (X509Certificate)
-                        certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+                lastCert = X509CertificateUtils.generateCertificate(encodedCert);
                 lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
                 if (certHistorySet.contains(lastCert)) {
                     throw new SecurityException("Encountered duplicate entries in "
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
index 8b1b59b..232bba3 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -129,14 +129,15 @@
                         || (container.getTagNumber() != expectedTagNumber)) {
                     throw new Asn1UnexpectedTagException(
                             "Unexpected data value read as " + containerClass.getName()
-                            + ". Expected " + BerEncoding.tagClassAndNumberToString(
+                                    + ". Expected " + BerEncoding.tagClassAndNumberToString(
                                     expectedTagClass, expectedTagNumber)
-                            + ", but read: " + BerEncoding.tagClassAndNumberToString(
+                                    + ", but read: " + BerEncoding.tagClassAndNumberToString(
                                     container.getTagClass(), container.getTagNumber()));
                 }
                 return parseSequence(container, containerClass);
             }
-
+            case UNENCODED_CONTAINER:
+                return parseSequence(container, containerClass, true);
             default:
                 throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
         }
@@ -177,7 +178,6 @@
         } catch (IllegalArgumentException | ReflectiveOperationException e) {
             throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
         }
-
         // Set the matching field's value from the data value
         for (AnnotatedField field : fields) {
             try {
@@ -194,6 +194,11 @@
 
     private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
             throws Asn1DecodingException {
+        return parseSequence(container, containerClass, false);
+    }
+
+    private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
+            boolean isUnencodedContainer) throws Asn1DecodingException {
         List<AnnotatedField> fields = getAnnotatedFields(containerClass);
         Collections.sort(
                 fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
@@ -226,7 +231,13 @@
         while (nextUnreadFieldIndex < fields.size()) {
             BerDataValue dataValue;
             try {
-                dataValue = elementsReader.readDataValue();
+                // if this is the first field of an unencoded container then the entire contents of
+                // the container should be used when assigning to this field.
+                if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
+                    dataValue = container;
+                } else {
+                    dataValue = elementsReader.readDataValue();
+                }
             } catch (BerDataValueFormatException e) {
                 throw new Asn1DecodingException("Malformed data value", e);
             }
@@ -310,6 +321,7 @@
         switch (containerAnnotation.type()) {
             case CHOICE:
             case SEQUENCE:
+            case UNENCODED_CONTAINER:
                 return containerAnnotation.type();
             default:
                 throw new Asn1DecodingException(
@@ -591,7 +603,6 @@
             } else if (Asn1OpaqueObject.class.equals(targetType)) {
                 return (T) new Asn1OpaqueObject(dataValue.getEncoded());
             }
-
             ByteBuffer encodedContents = dataValue.getEncodedContents();
             switch (sourceType) {
                 case INTEGER:
@@ -608,6 +619,29 @@
                         return (T) oidToString(encodedContents);
                     }
                     break;
+                case UTC_TIME:
+                case GENERALIZED_TIME:
+                    if (String.class.equals(targetType)) {
+                        return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
+                    }
+                case BOOLEAN:
+                    // A boolean should be encoded in a single byte with a value of 0 for false and
+                    // any non-zero value for true.
+                    if (boolean.class.equals(targetType)) {
+                        if (encodedContents.remaining() != 1) {
+                            throw new Asn1DecodingException(
+                                    "Incorrect encoded size of boolean value: "
+                                            + encodedContents.remaining());
+                        }
+                        boolean result;
+                        if (encodedContents.get() == 0) {
+                            result = false;
+                        } else {
+                            result = true;
+                        }
+                        return (T) new Boolean(result);
+
+                    }
                 case SEQUENCE:
                 {
                     Asn1Class containerAnnotation =
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
index a913d6d..da87368 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
@@ -17,6 +17,7 @@
 package com.android.apksig.internal.asn1;
 
 import com.android.apksig.internal.asn1.ber.BerEncoding;
+
 import java.io.ByteArrayOutputStream;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
@@ -64,6 +65,8 @@
                 return toChoice(container);
             case SEQUENCE:
                 return toSequence(container);
+            case UNENCODED_CONTAINER:
+                return toSequence(container, true);
             default:
                 throw new Asn1EncodingException("Unsupported container type: " + containerType);
         }
@@ -101,6 +104,11 @@
     }
 
     private static byte[] toSequence(Object container) throws Asn1EncodingException {
+        return toSequence(container, false);
+    }
+
+    private static byte[] toSequence(Object container, boolean omitTag)
+            throws Asn1EncodingException {
         Class<?> containerClass = container.getClass();
         List<AnnotatedField> fields = getAnnotatedFields(container);
         Collections.sort(
@@ -120,6 +128,7 @@
         }
 
         List<byte[]> serializedFields = new ArrayList<>(fields.size());
+        int contentLen = 0;
         for (AnnotatedField field : fields) {
             byte[] serializedField;
             try {
@@ -132,25 +141,50 @@
             }
             if (serializedField != null) {
                 serializedFields.add(serializedField);
+                contentLen += serializedField.length;
             }
         }
 
-        return createTag(
-                BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
-                serializedFields.toArray(new byte[0][]));
+        if (omitTag) {
+            byte[] unencodedResult = new byte[contentLen];
+            int index = 0;
+            for (byte[] serializedField : serializedFields) {
+                System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length);
+                index += serializedField.length;
+            }
+            return unencodedResult;
+        } else {
+            return createTag(
+                    BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
+                    serializedFields.toArray(new byte[0][]));
+        }
     }
 
-    private static byte[] toSetOf(Collection<?> values, Asn1Type elementType)
+    private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
+        return toSequenceOrSetOf(values, elementType, true);
+    }
+
+    private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
+        return toSequenceOrSetOf(values, elementType, false);
+    }
+
+    private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet)
             throws Asn1EncodingException {
         List<byte[]> serializedValues = new ArrayList<>(values.size());
         for (Object value : values) {
             serializedValues.add(JavaToDerConverter.toDer(value, elementType, null));
         }
-        if (serializedValues.size() > 1) {
-            Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
+        int tagNumber;
+        if (toSet) {
+            if (serializedValues.size() > 1) {
+                Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
+            }
+            tagNumber = BerEncoding.TAG_NUMBER_SET;
+        } else {
+            tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE;
         }
         return createTag(
-                BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SET,
+                BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
                 serializedValues.toArray(new byte[0][]));
     }
 
@@ -220,6 +254,18 @@
                 value.toByteArray());
     }
 
+    private static byte[] toBoolean(boolean value) {
+        // A boolean should be encoded in a single byte with a value of 0 for false and any non-zero
+        // value for true.
+        byte[] result = new byte[1];
+        if (value == false) {
+            result[0] = 0;
+        } else {
+            result[0] = 1;
+        }
+        return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result);
+    }
+
     private static byte[] toOid(String oid) throws Asn1EncodingException {
         ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
         String[] nodes = oid.split("\\.");
@@ -469,6 +515,7 @@
 
             switch (targetType) {
                 case OCTET_STRING:
+                case BIT_STRING:
                     byte[] value = null;
                     if (source instanceof ByteBuffer) {
                         ByteBuffer buf = (ByteBuffer) source;
@@ -481,7 +528,7 @@
                         return createTag(
                                 BerEncoding.TAG_CLASS_UNIVERSAL,
                                 false,
-                                BerEncoding.TAG_NUMBER_OCTET_STRING,
+                                BerEncoding.getTagNumber(targetType),
                                 value);
                     }
                     break;
@@ -494,6 +541,17 @@
                         return toInteger((BigInteger) source);
                     }
                     break;
+                case BOOLEAN:
+                    if (source instanceof Boolean) {
+                        return toBoolean((Boolean) (source));
+                    }
+                    break;
+                case UTC_TIME:
+                case GENERALIZED_TIME:
+                    if (source instanceof String) {
+                        return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false,
+                                BerEncoding.getTagNumber(targetType), ((String) source).getBytes());
+                    }
                 case OBJECT_IDENTIFIER:
                     if (source instanceof String) {
                         return toOid((String) source);
@@ -521,6 +579,8 @@
                 }
                 case SET_OF:
                     return toSetOf((Collection<?>) source, targetElementType);
+                case SEQUENCE_OF:
+                    return toSequenceOf((Collection<?>) source, targetElementType);
                 default:
                     break;
             }
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java b/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
index 710df71..7300622 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
@@ -25,4 +25,11 @@
     SEQUENCE,
     SEQUENCE_OF,
     SET_OF,
+    BIT_STRING,
+    UTC_TIME,
+    GENERALIZED_TIME,
+    BOOLEAN,
+    // This type can be used to annotate classes that encapsulate ASN.1 structures that are not
+    // classified as a SEQUENCE or SET.
+    UNENCODED_CONTAINER
 }
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
index 5fd0b52..d32330c 100644
--- a/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
@@ -51,11 +51,21 @@
     public static final int TAG_CLASS_PRIVATE = 3;
 
     /**
+     * Tag number: BOOLEAN
+     */
+    public static final int TAG_NUMBER_BOOLEAN = 0x1;
+
+    /**
      * Tag number: INTEGER
      */
     public static final int TAG_NUMBER_INTEGER = 0x2;
 
     /**
+     * Tag number: BIT STRING
+     */
+    public static final int TAG_NUMBER_BIT_STRING = 0x3;
+
+    /**
      * Tag number: OCTET STRING
      */
     public static final int TAG_NUMBER_OCTET_STRING = 0x4;
@@ -80,6 +90,16 @@
      */
     public static final int TAG_NUMBER_SET = 0x11;
 
+    /**
+     * Tag number: UTC_TIME
+     */
+    public final static int TAG_NUMBER_UTC_TIME = 0x17;
+
+    /**
+     * Tag number: GENERALIZED_TIME
+     */
+    public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18;
+
     public static int getTagNumber(Asn1Type dataType) {
         switch (dataType) {
             case INTEGER:
@@ -88,11 +108,19 @@
                 return TAG_NUMBER_OBJECT_IDENTIFIER;
             case OCTET_STRING:
                 return TAG_NUMBER_OCTET_STRING;
+            case BIT_STRING:
+                return TAG_NUMBER_BIT_STRING;
             case SET_OF:
                 return TAG_NUMBER_SET;
             case SEQUENCE:
             case SEQUENCE_OF:
                 return TAG_NUMBER_SEQUENCE;
+            case UTC_TIME:
+                return TAG_NUMBER_UTC_TIME;
+            case GENERALIZED_TIME:
+                return TAG_NUMBER_GENERALIZED_TIME;
+            case BOOLEAN:
+                return TAG_NUMBER_BOOLEAN;
             default:
                 throw new IllegalArgumentException("Unsupported data type: " + dataType);
         }
@@ -141,6 +169,8 @@
                 return "INTEGER";
             case TAG_NUMBER_OCTET_STRING:
                 return "OCTET STRING";
+            case TAG_NUMBER_BIT_STRING:
+                return "BIT STRING";
             case TAG_NUMBER_NULL:
                 return "NULL";
             case TAG_NUMBER_OBJECT_IDENTIFIER:
@@ -149,6 +179,12 @@
                 return "SEQUENCE";
             case TAG_NUMBER_SET:
                 return "SET";
+            case TAG_NUMBER_BOOLEAN:
+                return "BOOLEAN";
+            case TAG_NUMBER_GENERALIZED_TIME:
+                return "GENERALIZED TIME";
+            case TAG_NUMBER_UTC_TIME:
+                return "UTC TIME";
             default:
                 return "0x" + Integer.toHexString(tagNumber);
         }
diff --git a/src/test/java/com/android/apksig/internal/util/ByteStreams.java b/src/main/java/com/android/apksig/internal/util/ByteStreams.java
similarity index 100%
rename from src/test/java/com/android/apksig/internal/util/ByteStreams.java
rename to src/main/java/com/android/apksig/internal/util/ByteStreams.java
diff --git a/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
new file mode 100644
index 0000000..9a266f2
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import com.android.apksig.internal.asn1.Asn1BerParser;
+import com.android.apksig.internal.asn1.Asn1DecodingException;
+import com.android.apksig.internal.asn1.Asn1DerEncoder;
+import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.x509.Certificate;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+
+/**
+ * Provides methods to generate {@code X509Certificate}s from their encoded form. These methods
+ * can be used to generate certificates that would be rejected by the Java {@code
+ * CertificateFactory}.
+ */
+public class X509CertificateUtils {
+
+    private static CertificateFactory sCertFactory = null;
+
+    // The PEM certificate header and footer as specified in RFC 7468:
+    //   There is exactly one space character (SP) separating the "BEGIN" or
+    //   "END" from the label.  There are exactly five hyphen-minus (also
+    //   known as dash) characters ("-") on both ends of the encapsulation
+    //   boundaries, no more, no less.
+    public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes();
+    public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes();
+
+    private static void buildCertFactory() {
+        if (sCertFactory != null) {
+            return;
+        }
+        try {
+            sCertFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
+        }
+    }
+
+    /**
+     * Generates an {@code X509Certificate} from the {@code InputStream}.
+     *
+     * @throws CertificateException if the {@code InputStream} cannot be decoded to a valid
+     *                              certificate.
+     */
+    public static X509Certificate generateCertificate(InputStream in) throws CertificateException {
+        byte[] encodedForm;
+        try {
+            encodedForm = ByteStreams.toByteArray(in);
+        } catch (IOException e) {
+            throw new CertificateException("Failed to parse certificate", e);
+        }
+        return generateCertificate(encodedForm);
+    }
+
+    /**
+     * Generates an {@code X509Certificate} from the encoded form.
+     *
+     * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
+     */
+    public static X509Certificate generateCertificate(byte[] encodedForm)
+            throws CertificateException {
+        if (sCertFactory == null) {
+            buildCertFactory();
+        }
+        return generateCertificate(encodedForm, sCertFactory);
+    }
+
+    /**
+     * Generates an {@code X509Certificate} from the encoded form using the provided
+     * {@code CertificateFactory}.
+     *
+     * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
+     */
+    public static X509Certificate generateCertificate(byte[] encodedForm,
+            CertificateFactory certFactory) throws CertificateException {
+        X509Certificate certificate;
+        try {
+            certificate = (X509Certificate) certFactory.generateCertificate(
+                    new ByteArrayInputStream(encodedForm));
+            return certificate;
+        } catch (CertificateException e) {
+            // This could be expected if the certificate is encoded using a BER encoding that does
+            // not use the minimum number of bytes to represent the length of the contents; attempt
+            // to decode the certificate using the BER parser and re-encode using the DER encoder
+            // below.
+        }
+        try {
+            // Some apps were previously signed with a BER encoded certificate that now results
+            // in exceptions from the CertificateFactory generateCertificate(s) methods. Since
+            // the original BER encoding of the certificate is used as the signature for these
+            // apps that original encoding must be maintained when signing updated versions of
+            // these apps and any new apps that may require capabilities guarded by the
+            // signature. To maintain the same signature the BER parser can be used to parse
+            // the certificate, then it can be re-encoded to its DER equivalent which is
+            // accepted by the generateCertificate method. The positions in the ByteBuffer can
+            // then be used with the GuaranteedEncodedFormX509Certificate object to ensure the
+            // getEncoded method returns the original signature of the app.
+            ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock(
+                    ByteBuffer.wrap(encodedForm));
+            int startingPos = encodedCertBuffer.position();
+            Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class);
+            byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
+            certificate = (X509Certificate) certFactory.generateCertificate(
+                    new ByteArrayInputStream(reencodedForm));
+            // If the reencodedForm is successfully accepted by the CertificateFactory then copy the
+            // original encoding from the ByteBuffer and use that encoding in the Guaranteed object.
+            byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos];
+            encodedCertBuffer.position(startingPos);
+            encodedCertBuffer.get(originalEncoding);
+            GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
+                    new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
+            return guaranteedEncodedCert;
+        } catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) {
+            throw new CertificateException("Failed to parse certificate", e);
+        }
+    }
+
+    /**
+     * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
+     * InputStream}.
+     *
+     * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
+     *                              {@code Certificate} objects.
+     */
+    public static Collection<? extends java.security.cert.Certificate> generateCertificates(
+            InputStream in) throws CertificateException {
+        if (sCertFactory == null) {
+            buildCertFactory();
+        }
+        return generateCertificates(in, sCertFactory);
+    }
+
+    /**
+     * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
+     * InputStream} using the provided {@code CertificateFactory}.
+     *
+     * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
+     *                              {@code Certificates} objects.
+     */
+    public static Collection<? extends java.security.cert.Certificate> generateCertificates(
+            InputStream in, CertificateFactory certFactory) throws CertificateException {
+        // Since the InputStream is not guaranteed to support mark / reset operations first read it
+        // into a byte array to allow using the BER parser / DER encoder if it cannot be read by
+        // the CertificateFactory.
+        byte[] encodedCerts;
+        try {
+            encodedCerts = ByteStreams.toByteArray(in);
+        } catch (IOException e) {
+            throw new CertificateException("Failed to read the input stream", e);
+        }
+        try {
+            return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts));
+        } catch (CertificateException e) {
+            // This could be expected if the certificates are encoded using a BER encoding that does
+            // not use the minimum number of bytes to represent the length of the contents; attempt
+            // to decode the certificates using the BER parser and re-encode using the DER encoder
+            // below.
+        }
+        try {
+            Collection<X509Certificate> certificates = new ArrayList<>(1);
+            ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts);
+            while (encodedCertsBuffer.hasRemaining()) {
+                ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer);
+                int startingPos = certBuffer.position();
+                Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class);
+                byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
+                X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(
+                        new ByteArrayInputStream(reencodedForm));
+                byte[] originalEncoding = new byte[certBuffer.position() - startingPos];
+                certBuffer.position(startingPos);
+                certBuffer.get(originalEncoding);
+                GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
+                        new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
+                certificates.add(guaranteedEncodedCert);
+            }
+            return certificates;
+        } catch (Asn1DecodingException | Asn1EncodingException e) {
+            throw new CertificateException("Failed to parse certificates", e);
+        }
+    }
+
+    /**
+     * Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer
+     * does not begin with the PEM certificate header then it is returned with the assumption that
+     * it is already DER encoded. If the buffer does begin with the PEM certificate header then the
+     * certificate data is read from the buffer until the PEM certificate footer is reached; this
+     * data is then base64 decoded and returned in a new ByteBuffer.
+     *
+     * If the buffer is in PEM format then the position of the buffer is moved to the end of the
+     * current certificate; if the buffer is already DER encoded then the position of the buffer is
+     * not modified.
+     *
+     * @throws CertificateException if the buffer contains the PEM certificate header but does not
+     *                              contain the expected footer.
+     */
+    private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer)
+            throws CertificateException {
+        if (certificateBuffer == null) {
+            throw new NullPointerException("The certificateBuffer cannot be null");
+        }
+        // if the buffer does not contain enough data for the PEM cert header then just return the
+        // provided buffer.
+        if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) {
+            return certificateBuffer;
+        }
+        certificateBuffer.mark();
+        for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) {
+            if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) {
+                certificateBuffer.reset();
+                return certificateBuffer;
+            }
+        }
+        StringBuilder pemEncoding = new StringBuilder();
+        while (certificateBuffer.hasRemaining()) {
+            char encodedChar = (char) certificateBuffer.get();
+            // if the current character is a '-' then the beginning of the footer has been reached
+            if (encodedChar == '-') {
+                break;
+            } else if (Character.isWhitespace(encodedChar)) {
+                continue;
+            } else {
+                pemEncoding.append(encodedChar);
+            }
+        }
+        // start from the second index in the certificate footer since the first '-' should have
+        // been consumed above.
+        for (int i = 1; i < END_CERT_FOOTER.length; i++) {
+            if (!certificateBuffer.hasRemaining()) {
+                throw new CertificateException(
+                        "The provided input contains the PEM certificate header but does not "
+                                + "contain sufficient data for the footer");
+            }
+            if (certificateBuffer.get() != END_CERT_FOOTER[i]) {
+                throw new CertificateException(
+                        "The provided input contains the PEM certificate header without a "
+                                + "valid certificate footer");
+            }
+        }
+        byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString());
+        // consume any trailing whitespace in the byte buffer
+        int nextEncodedChar = certificateBuffer.position();
+        while (certificateBuffer.hasRemaining()) {
+            char trailingChar = (char) certificateBuffer.get();
+            if (Character.isWhitespace(trailingChar)) {
+                nextEncodedChar++;
+            } else {
+                break;
+            }
+        }
+        certificateBuffer.position(nextEncodedChar);
+        return ByteBuffer.wrap(derEncoding);
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java b/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java
new file mode 100644
index 0000000..077db23
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+/**
+ * {@code AttributeTypeAndValue} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class AttributeTypeAndValue {
+
+    @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+    public String attrType;
+
+    @Asn1Field(index = 1, type = Asn1Type.ANY)
+    public Asn1OpaqueObject attrValue;
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/apksig/internal/x509/Certificate.java b/src/main/java/com/android/apksig/internal/x509/Certificate.java
new file mode 100644
index 0000000..abb3c15
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Certificate.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+
+import java.nio.ByteBuffer;
+
+/**
+ * X509 {@code Certificate} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Certificate {
+    @Asn1Field(index = 0, type = Asn1Type.SEQUENCE)
+    public TBSCertificate certificate;
+
+    @Asn1Field(index = 1, type = Asn1Type.SEQUENCE)
+    public AlgorithmIdentifier signatureAlgorithm;
+
+    @Asn1Field(index = 2, type = Asn1Type.BIT_STRING)
+    public ByteBuffer signature;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Extension.java b/src/main/java/com/android/apksig/internal/x509/Extension.java
new file mode 100644
index 0000000..bf37c1e
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Extension.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * X509 {@code Extension} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Extension {
+    @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+    public String extensionID;
+
+    @Asn1Field(index = 1, type = Asn1Type.BOOLEAN, optional = true)
+    public boolean isCritial = false;
+
+    @Asn1Field(index = 2, type = Asn1Type.OCTET_STRING)
+    public ByteBuffer extensionValue;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Name.java b/src/main/java/com/android/apksig/internal/x509/Name.java
new file mode 100644
index 0000000..08400d6
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Name.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+import java.util.List;
+
+/**
+ * X501 {@code Name} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.CHOICE)
+public class Name {
+
+    // This field is the RDNSequence specified in RFC 5280.
+    @Asn1Field(index = 0, type = Asn1Type.SEQUENCE_OF)
+    public List<RelativeDistinguishedName> relativeDistinguishedNames;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java b/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java
new file mode 100644
index 0000000..bb89e8d
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+import java.util.List;
+
+/**
+ * {@code RelativeDistinguishedName} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.UNENCODED_CONTAINER)
+public class RelativeDistinguishedName {
+
+    @Asn1Field(index = 0, type = Asn1Type.SET_OF)
+    public List<AttributeTypeAndValue> attributes;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java b/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java
new file mode 100644
index 0000000..8215237
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@code SubjectPublicKeyInfo} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class SubjectPublicKeyInfo {
+    @Asn1Field(index = 0, type = Asn1Type.SEQUENCE)
+    public AlgorithmIdentifier algorithmIdentifier;
+
+    @Asn1Field(index = 1, type = Asn1Type.BIT_STRING)
+    public ByteBuffer subjectPublicKey;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java b/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java
new file mode 100644
index 0000000..922f52c
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.asn1.Asn1Tagging;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * To Be Signed Certificate as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class TBSCertificate {
+
+    @Asn1Field(
+            index = 0,
+            type = Asn1Type.INTEGER,
+            tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
+    public int version;
+
+    @Asn1Field(index = 1, type = Asn1Type.INTEGER)
+    public BigInteger serialNumber;
+
+    @Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
+    public AlgorithmIdentifier signatureAlgorithm;
+
+    @Asn1Field(index = 3, type = Asn1Type.CHOICE)
+    public Name issuer;
+
+    @Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
+    public Validity validity;
+
+    @Asn1Field(index = 5, type = Asn1Type.CHOICE)
+    public Name subject;
+
+    @Asn1Field(index = 6, type = Asn1Type.SEQUENCE)
+    public SubjectPublicKeyInfo subjectPublicKeyInfo;
+
+    @Asn1Field(index = 7,
+            type = Asn1Type.BIT_STRING,
+            tagging = Asn1Tagging.IMPLICIT,
+            optional = true,
+            tagNumber = 1)
+    public ByteBuffer issuerUniqueID;
+
+    @Asn1Field(index = 8,
+            type = Asn1Type.BIT_STRING,
+            tagging = Asn1Tagging.IMPLICIT,
+            optional = true,
+            tagNumber = 2)
+    public ByteBuffer subjectUniqueID;
+
+    @Asn1Field(index = 9,
+            type = Asn1Type.SEQUENCE_OF,
+            tagging = Asn1Tagging.EXPLICIT,
+            optional = true,
+            tagNumber = 3)
+    public List<Extension> extensions;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Time.java b/src/main/java/com/android/apksig/internal/x509/Time.java
new file mode 100644
index 0000000..def2ee8
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Time.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+/**
+ * {@code Time} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.CHOICE)
+public class Time {
+
+    @Asn1Field(type = Asn1Type.UTC_TIME)
+    public String utcTime;
+
+    @Asn1Field(type = Asn1Type.GENERALIZED_TIME)
+    public String generalizedTime;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Validity.java b/src/main/java/com/android/apksig/internal/x509/Validity.java
new file mode 100644
index 0000000..df9acb3
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Validity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+/**
+ * {@code Validity} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Validity {
+
+    @Asn1Field(index = 0, type = Asn1Type.CHOICE)
+    public Time notBefore;
+
+    @Asn1Field(index = 1, type = Asn1Type.CHOICE)
+    public Time notAfter;
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java b/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java
index 93bd3b4..1816204 100644
--- a/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java
+++ b/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java
@@ -61,6 +61,39 @@
     }
 
     @Test
+    public void testBitString() throws Exception {
+        assertEquals(
+                "123456",
+                HexEncoding.encode(parse("30050303123456", SequenceWithBitString.class).buf));
+        assertEquals(
+                "", HexEncoding.encode(parse("30020300", SequenceWithBitString.class).buf));
+    }
+
+    @Test
+    public void testBoolean() throws Exception {
+        assertEquals(false, parse("3003010100", SequenceWithBoolean.class).value);
+        assertEquals(true, parse("3003010101", SequenceWithBoolean.class).value);
+        assertEquals(true, parse("30030101FF", SequenceWithBoolean.class).value);
+    }
+
+    @Test
+    public void testUTCTime() throws Exception {
+        assertEquals("1212211221Z",
+                parse("300d170b313231323231313232315a", SequenceWithUTCTime.class).value);
+        assertEquals("9912312359Z",
+                parse("300d170b393931323331323335395a", SequenceWithUTCTime.class).value);
+    }
+
+    @Test
+    public void testGeneralizedTime() throws Exception {
+        assertEquals("201212211220.999-07", parse("301518133230313231323231313232302e3939392d3037",
+                SequenceWithGeneralizedTime.class).value);
+        assertEquals("20380119031407.000+00",
+                parse("3017181532303338303131393033313430372e3030302b3030",
+                        SequenceWithGeneralizedTime.class).value);
+    }
+
+    @Test
     public void testInteger() throws Exception {
         // Various Java types decoded from INTEGER
         // Empty SEQUENCE (0x3000) followed by garbage (0x12345678)
@@ -101,6 +134,15 @@
     }
 
     @Test
+    public void testUnencodedContainer() throws Exception {
+        SequenceWithSequenceOfUnencodedContainers seq = parse("300C300A31023000310430003000",
+                SequenceWithSequenceOfUnencodedContainers.class);
+        assertEquals(2, seq.containers.size());
+        assertEquals(1, seq.containers.get(0).values.size());
+        assertEquals(2, seq.containers.get(1).values.size());
+    }
+
+    @Test
     public void testImplicitOptionalField() throws Exception {
         // Optional field f2 missing in the input
         SequenceWithImplicitOptionalField seq =
@@ -361,6 +403,12 @@
     }
 
     @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithBitString {
+        @Asn1Field(index = 0, type = Asn1Type.BIT_STRING)
+        public ByteBuffer buf;
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
     public static class SequenceWithSequenceOf {
         @Asn1Field(index = 0, type = Asn1Type.SEQUENCE_OF)
         public List<EmptySequence> values;
@@ -377,4 +425,34 @@
         @Asn1Field(type = Asn1Type.ANY)
         public Asn1OpaqueObject obj;
     }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithSequenceOfUnencodedContainers {
+        @Asn1Field(type = Asn1Type.SEQUENCE_OF)
+        public List<UnencodedContainerWithSetOf> containers;
+    }
+
+    @Asn1Class(type = Asn1Type.UNENCODED_CONTAINER)
+    public static class UnencodedContainerWithSetOf {
+        @Asn1Field(type = Asn1Type.SET_OF)
+        public List<EmptySequence> values;
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithBoolean {
+        @Asn1Field(type = Asn1Type.BOOLEAN)
+        public boolean value;
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithUTCTime {
+        @Asn1Field(type = Asn1Type.UTC_TIME)
+        public String value;
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithGeneralizedTime {
+        @Asn1Field(type = Asn1Type.GENERALIZED_TIME)
+        public String value;
+    }
 }
diff --git a/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java b/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java
index ad4ec82..02edb23 100644
--- a/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java
+++ b/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java
@@ -17,6 +17,7 @@
 package com.android.apksig.internal.asn1;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.fail;
 
 import com.android.apksig.internal.util.HexEncoding;
@@ -63,6 +64,26 @@
     }
 
     @Test
+    public void testBitString() throws Exception {
+        assertEquals(
+                "30050303010203",
+                encodeToHex(
+                        new SequenceWithByteBufferBitString(
+                                ByteBuffer.wrap(new byte[] {1, 2, 3}))));
+        assertEquals(
+                "30030301ff",
+                encodeToHex(
+                        new SequenceWithByteBufferBitString(
+                                ByteBuffer.wrap(new byte[] {(byte) 0xff}))));
+
+        assertEquals(
+                "30020300",
+                encodeToHex(
+                        new SequenceWithByteBufferBitString(ByteBuffer.wrap(new byte[0]))));
+    }
+
+
+    @Test
     public void testOid() throws Exception {
         assertEquals("3003060100", encodeToHex(new SequenceWithOid("0.0")));
         assertEquals(
@@ -138,6 +159,57 @@
                         new Asn1OpaqueObject(new byte[] {0x06, 0x01, 0x00}))));
     }
 
+    @Test
+    public void testBoolean() throws Exception {
+        assertEquals("3003010100", encodeToHex(new SequenceWithBoolean(false)));
+        String value = encodeToHex(new SequenceWithBoolean(true));
+        // The encoding of a true value can be any non-zero value so verify the static portion of
+        // the encoding of a sequeuence with a boolean, then verify the last byte is non-zero
+        assertEquals("The encoding of a sequence with a boolean is not the expected length.", 10,
+                value.length());
+        assertEquals(
+                "The prefix of the encoding of a sequence with a boolean is not the expected "
+                        + "value.",
+                "30030101", value.substring(0, 8));
+        assertNotEquals("The encoding of true should be non-zero.", "00", value.substring(8));
+    }
+
+    @Test
+    public void testUTCTime() throws Exception {
+        assertEquals("300d170b313231323231313232315a",
+                encodeToHex(new SequenceWithUTCTime("1212211221Z")));
+        assertEquals("300d170b393931323331323335395a",
+                encodeToHex(new SequenceWithUTCTime("9912312359Z")));
+    }
+
+    @Test
+    public void testGeneralizedTime() throws Exception {
+        assertEquals("301518133230313231323231313232302e3939392d3037",
+                encodeToHex(new SequenceWithGeneralizedTime("201212211220.999-07")));
+        assertEquals("3017181532303338303131393033313430372e3030302b3030",
+                encodeToHex(new SequenceWithGeneralizedTime("20380119031407.000+00")));
+    }
+
+    @Test
+    public void testUnencodedContainer() throws Exception {
+        assertEquals("30233021310b30030201003004020200ff310830060204800000003108300602047fffffff",
+                encodeToHex(
+                        new SequenceWithSequenceOfUnencodedContainers(
+                                Arrays.asList(
+                                        new UnencodedContainerWithSetOfIntegers(
+                                                Arrays.asList(
+                                                        new SequenceWithInteger(0),
+                                                        new SequenceWithInteger(255))),
+                                        new UnencodedContainerWithSetOfIntegers(
+                                                Arrays.asList(
+                                                        new SequenceWithInteger(
+                                                                Integer.MIN_VALUE))),
+                                        new UnencodedContainerWithSetOfIntegers(
+                                                Arrays.asList(
+                                                        new SequenceWithInteger(
+                                                                Integer.MAX_VALUE)))))));
+    }
+
     private static byte[] encode(Object obj) throws Asn1EncodingException {
         return Asn1DerEncoder.encode(obj);
     }
@@ -180,6 +252,17 @@
         }
     }
 
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithByteBufferBitString {
+
+        @Asn1Field(index = 1, type = Asn1Type.BIT_STRING)
+        public ByteBuffer data;
+
+        public SequenceWithByteBufferBitString(ByteBuffer data) {
+            this.data = data;
+        }
+    }
+
     @Asn1Class(type = Asn1Type.CHOICE)
     public static class Choice {
 
@@ -252,4 +335,58 @@
             this.obj = obj;
         }
     }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithBoolean {
+
+        @Asn1Field(index = 1, type = Asn1Type.BOOLEAN)
+        public boolean value;
+
+        public SequenceWithBoolean(boolean value) {
+            this.value = value;
+        }
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithUTCTime {
+
+        @Asn1Field(index = 1, type = Asn1Type.UTC_TIME)
+        public String utcTime;
+
+        public SequenceWithUTCTime(String utcTime) {
+            this.utcTime = utcTime;
+        }
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithGeneralizedTime {
+
+        @Asn1Field(index = 1, type = Asn1Type.GENERALIZED_TIME)
+        public String generalizedTime;
+
+        public SequenceWithGeneralizedTime(String generalizedTime) {
+            this.generalizedTime = generalizedTime;
+        }
+    }
+
+    @Asn1Class(type = Asn1Type.SEQUENCE)
+    public static class SequenceWithSequenceOfUnencodedContainers {
+        @Asn1Field(index = 1, type = Asn1Type.SEQUENCE_OF)
+        public List<UnencodedContainerWithSetOfIntegers> containers;
+
+        public SequenceWithSequenceOfUnencodedContainers(
+                List<UnencodedContainerWithSetOfIntegers> containers) {
+            this.containers = containers;
+        }
+    }
+
+    @Asn1Class(type = Asn1Type.UNENCODED_CONTAINER)
+    public static class UnencodedContainerWithSetOfIntegers {
+        @Asn1Field(index = 1, type = Asn1Type.SET_OF)
+        public List<SequenceWithInteger> values;
+
+        public UnencodedContainerWithSetOfIntegers(List<SequenceWithInteger> values) {
+            this.values = values;
+        }
+    }
 }
diff --git a/src/test/java/com/android/apksig/internal/util/AllTests.java b/src/test/java/com/android/apksig/internal/util/AllTests.java
index b78acde..78a6aa4 100644
--- a/src/test/java/com/android/apksig/internal/util/AllTests.java
+++ b/src/test/java/com/android/apksig/internal/util/AllTests.java
@@ -25,5 +25,6 @@
     ChainedDataSourceTest.class,
     DirectByteBufferSinkTest.class,
     VerityTreeBuilderTest.class,
+    X509CertificateUtilsTest.class,
 })
 public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/util/Resources.java b/src/test/java/com/android/apksig/internal/util/Resources.java
index 09c206d..82bf76f 100644
--- a/src/test/java/com/android/apksig/internal/util/Resources.java
+++ b/src/test/java/com/android/apksig/internal/util/Resources.java
@@ -28,7 +28,6 @@
 import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
@@ -52,14 +51,21 @@
         }
     }
 
+    public static InputStream toInputStream(Class<?> cls, String resourceName) throws IOException {
+            InputStream in = cls.getResourceAsStream(resourceName);
+            if (in == null) {
+                throw new IllegalArgumentException("Resource not found: " + resourceName);
+            }
+            return in;
+    }
+
     public static X509Certificate toCertificate(
             Class <?> cls, String resourceName) throws IOException, CertificateException {
         try (InputStream in = cls.getResourceAsStream(resourceName)) {
             if (in == null) {
                 throw new IllegalArgumentException("Resource not found: " + resourceName);
             }
-            return (X509Certificate)
-                    CertificateFactory.getInstance("X.509").generateCertificate(in);
+            return X509CertificateUtils.generateCertificate(in);
         }
     }
 
@@ -70,7 +76,7 @@
             if (in == null) {
                 throw new IllegalArgumentException("Resource not found: " + resourceName);
             }
-            certs = CertificateFactory.getInstance("X.509").generateCertificates(in);
+            certs = X509CertificateUtils.generateCertificates(in);
         }
         List<X509Certificate> result = new ArrayList<>(certs.size());
         for (Certificate cert : certs) {
diff --git a/src/test/java/com/android/apksig/internal/util/X509CertificateUtilsTest.java b/src/test/java/com/android/apksig/internal/util/X509CertificateUtilsTest.java
new file mode 100644
index 0000000..daf6739
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/util/X509CertificateUtilsTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class X509CertificateUtilsTest {
+    // The PEM and DER encodings of a certificate without redundant length bytes; since the
+    // certificates are the same they have the same hex encoding of their digest.
+    public static final String RSA_2048_VALID_PEM_ENCODING = "rsa-2048.x509.pem";
+    public static final String RSA_2048_VALID_DER_ENCODING = "rsa-2048.x509.der";
+    public static final String RSA_2048_VALID_DIGEST_HEX_ENCODING =
+            "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+
+    // The PEM and DER encodings of a certificate with redundant length bytes; valid DER encodings
+    // require that the length of contents within the encoding be specified with the minimum number
+    // of bytes, but BER encodings allow redundant '00' bytes when specifying length.
+    public static final String RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING =
+            "rsa-2048-redun-len.x509.pem";
+    public static final String RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING =
+            "rsa-2048-redun-len.x509.der";
+    public static final String RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING =
+            "38481f124f8af6c36017abdfbefe375157ac304fb90adaa641ecba71b08dcd0f";
+
+    // The PEM and DER encodings of both the valid and redundant length byte certificates above.
+    public static final String RSA_2048_TWO_CERTS_PEM_ENCODING = "rsa-2048-2-certs.x509.pem";
+    public static final String RSA_2048_TWO_CERTS_DER_ENCODING = "rsa-2048-2-certs.x509.der";
+
+    @Test
+    public void testGenerateCertificateWithValidPEMEncoding() throws Exception {
+        // The generateCertificate method should support both PEM and DER encodings; since the PEM
+        // format is just the DER encoding base64'd with a header and a footer this test verifies
+        // that a valid DER encoding in PEM format is successfully parsed and returns the expected
+        // encoding.
+        assertEquals(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+                getHexEncodedDigestOfCertFromResources(RSA_2048_VALID_PEM_ENCODING));
+    }
+
+    @Test
+    public void testGenerateCertificateWithRedundantLengthBytesInPEMEncoding() throws Exception {
+        // This test verifies that a BER encoding of a certificate with redundant length bytes
+        // can still be successfully parsed and returns the expected unmodified encoding.
+        assertEquals(RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING,
+                getHexEncodedDigestOfCertFromResources(RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING));
+    }
+
+    @Test
+    public void testGenerateCertificateWithValidDEREncoding() throws Exception {
+        // This test verifies the generateCertificate method successfully parses and returns the
+        // expected encoding of a certificate with a valid DER encoding.
+        assertEquals(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+                getHexEncodedDigestOfCertFromResources(RSA_2048_VALID_DER_ENCODING));
+    }
+
+    @Test
+    public void testGenerateCertificateWithRedundantLengthBytesInDERENcoding() throws Exception {
+        // This test verifies the generateCertificate method successfully parses and returns the
+        // original encoding of a certificate with redundant length bytes in the encoding.
+        assertEquals(RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING,
+                getHexEncodedDigestOfCertFromResources(RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING));
+    }
+
+    @Test
+    public void testGenerateCertificatesWithTwoPEMEncodedCerts() throws Exception {
+        // The generateCertificates method accepts an InputStream which could contain zero or more
+        // certificates in PEM or DER encoding; this test verifies both certificates in PEM format
+        // are returned with the expected encodings.
+        List<String> encodedCerts = getHexEncodedDigestsOfCertsFromResources(
+                RSA_2048_TWO_CERTS_PEM_ENCODING);
+        Set<String> expectedEncodings = createSetOfValues(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+                RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING);
+        assertEncodingsMatchExpectedValues(encodedCerts, expectedEncodings);
+    }
+
+    @Test
+    public void testGenerateCertificatesWithTwoDEREncodedCerts() throws Exception {
+        // This test verifies the generateCertificates method returns the expected encodings for
+        // an InputStream with both DER encoded certificates.
+        List<String> encodedCerts = getHexEncodedDigestsOfCertsFromResources(
+                RSA_2048_TWO_CERTS_DER_ENCODING);
+        Set<String> expectedEncodings = createSetOfValues(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+                RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING);
+        assertEncodingsMatchExpectedValues(encodedCerts, expectedEncodings);
+    }
+
+    @Test
+    public void testGenerateCertificateAndGenerateCertificatesReturnSameValues() throws Exception {
+        // The generateCertificates method is intended to read multiple certificates in the provided
+        // InputStream, but it can also read a single certificate. Verify that both
+        // generateCertificate and generateCertificates return the same encodings for the same
+        // certificates.
+        List<String> certResources = Arrays.asList(RSA_2048_VALID_PEM_ENCODING,
+                RSA_2048_VALID_DER_ENCODING, RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING,
+                RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING);
+        for (String certResource : certResources) {
+            String genCertValue = getHexEncodedDigestOfCertFromResources(certResource);
+            List<String> genCertsValues = getHexEncodedDigestsOfCertsFromResources(certResource);
+            assertEquals(
+                    "The generateCertificates method should have returned a single certificate", 1,
+                    genCertsValues.size());
+            assertEquals(
+                    "The hex encoded digest of the certificate from generateCertificate does not "
+                            + "match the value from generateCertificates",
+                    genCertValue, genCertsValues.get(0));
+        }
+    }
+
+    @Test
+    public void testGenerateCertificatesWithEmptyInput() throws Exception {
+        // This test verifies the generateCertificates method returns an empty Collection of
+        // Certificates when provided an empty InputStream.
+        assertEquals(
+                "Zero certificates should be returned when passing an empty InputStream to "
+                        + "generateCertificates",
+                0, X509CertificateUtils.generateCertificates(
+                        new ByteArrayInputStream(new byte[0])).size());
+    }
+
+    private static Set<String> createSetOfValues(String... values) {
+        Set<String> result = new HashSet<>();
+        for (String value : values) {
+            result.add(value);
+        }
+        return result;
+    }
+
+    /**
+     * Returns a hex encoding of the digest of the specified certificate from the resources.
+     */
+    private static String getHexEncodedDigestOfCertFromResources(String resourceName)
+            throws Exception {
+        byte[] encodedForm = Resources.toByteArray(X509CertificateUtilsTest.class, resourceName);
+        X509Certificate cert = X509CertificateUtils.generateCertificate(encodedForm);
+        return getHexEncodedDigestOfBytes(cert.getEncoded());
+    }
+
+    /**
+     * Returns a list of hex encodings of the digests of the certificates in the specified resource.
+     */
+    private static List<String> getHexEncodedDigestsOfCertsFromResources(String resourceName)
+            throws Exception {
+        InputStream in = Resources.toInputStream(X509CertificateUtilsTest.class, resourceName);
+        Collection<? extends Certificate> certs = X509CertificateUtils.generateCertificates(in);
+        List<String> encodedCerts = new ArrayList<>(certs.size());
+        for (Certificate cert : certs) {
+            encodedCerts.add(getHexEncodedDigestOfBytes(cert.getEncoded()));
+        }
+        return encodedCerts;
+    }
+
+    /**
+     * Returns the hex encoding of the digest of the specified bytes.
+     */
+    private static String getHexEncodedDigestOfBytes(byte[] bytes)
+            throws NoSuchAlgorithmException {
+        return HexEncoding.encode(MessageDigest.getInstance("SHA-256").digest(bytes));
+    }
+
+    /**
+     * Asserts that the encoding of the provided certificates match the expected values.
+     */
+    private static void assertEncodingsMatchExpectedValues(List<String> encodedCerts,
+            Set<String> expectedValues) {
+        assertEquals(
+                "The number of encoded certificates does not match the expected number of values",
+                expectedValues.size(), encodedCerts.size());
+        for (String encodedCert : encodedCerts) {
+            // if the current encoding is found in the expected Set then remove it to ensure that
+            // duplicate values do not cause the test to pass if they are not expected.
+            if (expectedValues.contains(encodedCert)) {
+                expectedValues.remove(encodedCert);
+            } else {
+                fail("An unexpected certificate with the following encoding was returned: "
+                        + encodedCert);
+            }
+        }
+    }
+}
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.der b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.der
new file mode 100644
index 0000000..6464e3a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.der
Binary files differ
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.pem b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.pem
new file mode 100644
index 0000000..0659079
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.pem
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAeGgAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDQ4JI1EJ239V4wss0jpVlZMudh2/kARCVdoBgsRQuvc2RNnO23Eyynlt9UN+Dc
+NRdQIhbCpVTjdEl/bePECHlqg9NE3frAj5GebiUdWL6A/idKsZA1nAKyIgxxjcnu
++38OcrlO6XOm36euxGfd/ULrghZGXzMVFq4uLiIv3DqFkUcIlE0BvUiUoNwpopV4
+MKj1GQgoaEObJG5xkMBKO6vg36VfJ3s3V3r48uJxYGhhBZEB0EpoXLd4i0piAB8S
+MLb0Ek6wA/HZ8A2rdnStk1wl/83OM1jO0uB3hyfJpqIijlvNGnrloYyyOIqS0LGH
+nxSJD7goASH2Ef0h4yxbsOvHAgMBAAGjUDBOMB0GA1UdDgQWBBQXAi1zEH84mzkS
+62ohswGGWSwdbzAfBgNVHSMEGDAWgBQXAi1zEH84mzkS62ohswGGWSwdbzAMBgNV
+HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB92T5toLkF6dLl65/boH5Qvub
+5wfIk0AD12T3t3kYWQFOH0YDCHNL3SfmrjYM/CwNJAd1KuCL5AZcn0km/n0SFXt5
+8Ps/MBcb0eK1fYezeEehKUyt5IBgDTKeQOel6So8rGuQRrDf/WV8rt6fugkIODFx
+sB3oj4ESaGXbvmvWD6q4a3koq/nV26kALchnAr7/FTNq3HEIQ1BDr9pldVh1gEV/
+ohHKcQP4M22Es7lredzpIcb5K6Ko/UtwsSRtHnoOjwmb+L/FsgAJsekmcJG5TK1X
+ciIsrrNFDCYzf/d9O1PD/V95kB7460qMzrGWZpc3mLe+OnmVMq6c4omOtIKl
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAeugAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEjMA0GCSqGSIb3DQEBAQUAA4MAAQ8AMIIBCgKC
+AQEA0OCSNRCdt/VeMLLNI6VZWTLnYdv5AEQlXaAYLEULr3NkTZzttxMsp5bfVDfg
+3DUXUCIWwqVU43RJf23jxAh5aoPTRN36wI+Rnm4lHVi+gP4nSrGQNZwCsiIMcY3J
+7vt/DnK5Tulzpt+nrsRn3f1C64IWRl8zFRauLi4iL9w6hZFHCJRNAb1IlKDcKaKV
+eDCo9RkIKGhDmyRucZDASjur4N+lXyd7N1d6+PLicWBoYQWRAdBKaFy3eItKYgAf
+EjC29BJOsAPx2fANq3Z0rZNcJf/NzjNYztLgd4cnyaaiIo5bzRp65aGMsjiKktCx
+h58UiQ+4KAEh9hH9IeMsW7DrxwIDAQABo1kwVzAgBgNVHQ4BAQAEFgQUFwItcxB/
+OJs5EutqIbMBhlksHW8wIgYDVR0jAQEABBgwFoAUFwItcxB/OJs5EutqIbMBhlks
+HW8wDwYDVR0TAQEABAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAfdk+baC5Ben
+S5euf26B+UL7m+cHyJNAA9dk97d5GFkBTh9GAwhzS90n5q42DPwsDSQHdSrgi+QG
+XJ9JJv59EhV7efD7PzAXG9HitX2Hs3hHoSlMreSAYA0ynkDnpekqPKxrkEaw3/1l
+fK7en7oJCDgxcbAd6I+BEmhl275r1g+quGt5KKv51dupAC3IZwK+/xUzatxxCENQ
+Q6/aZXVYdYBFf6IRynED+DNthLO5a3nc6SHG+SuiqP1LcLEkbR56Do8Jm/i/xbIA
+CbHpJnCRuUytV3IiLK6zRQwmM3/3fTtTw/1feZAe+OtKjM6xlmaXN5i3vjp5lTKu
+nOKJjrSCpQ==
+-----END CERTIFICATE-----
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.der b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.der
new file mode 100644
index 0000000..19b3f89
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.der
Binary files differ
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.pem b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.pem
new file mode 100644
index 0000000..a1ddac7
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAeugAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEjMA0GCSqGSIb3DQEBAQUAA4MAAQ8AMIIBCgKC
+AQEA0OCSNRCdt/VeMLLNI6VZWTLnYdv5AEQlXaAYLEULr3NkTZzttxMsp5bfVDfg
+3DUXUCIWwqVU43RJf23jxAh5aoPTRN36wI+Rnm4lHVi+gP4nSrGQNZwCsiIMcY3J
+7vt/DnK5Tulzpt+nrsRn3f1C64IWRl8zFRauLi4iL9w6hZFHCJRNAb1IlKDcKaKV
+eDCo9RkIKGhDmyRucZDASjur4N+lXyd7N1d6+PLicWBoYQWRAdBKaFy3eItKYgAf
+EjC29BJOsAPx2fANq3Z0rZNcJf/NzjNYztLgd4cnyaaiIo5bzRp65aGMsjiKktCx
+h58UiQ+4KAEh9hH9IeMsW7DrxwIDAQABo1kwVzAgBgNVHQ4BAQAEFgQUFwItcxB/
+OJs5EutqIbMBhlksHW8wIgYDVR0jAQEABBgwFoAUFwItcxB/OJs5EutqIbMBhlks
+HW8wDwYDVR0TAQEABAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAfdk+baC5Ben
+S5euf26B+UL7m+cHyJNAA9dk97d5GFkBTh9GAwhzS90n5q42DPwsDSQHdSrgi+QG
+XJ9JJv59EhV7efD7PzAXG9HitX2Hs3hHoSlMreSAYA0ynkDnpekqPKxrkEaw3/1l
+fK7en7oJCDgxcbAd6I+BEmhl275r1g+quGt5KKv51dupAC3IZwK+/xUzatxxCENQ
+Q6/aZXVYdYBFf6IRynED+DNthLO5a3nc6SHG+SuiqP1LcLEkbR56Do8Jm/i/xbIA
+CbHpJnCRuUytV3IiLK6zRQwmM3/3fTtTw/1feZAe+OtKjM6xlmaXN5i3vjp5lTKu
+nOKJjrSCpQ==
+-----END CERTIFICATE-----
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.der b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.der
new file mode 100644
index 0000000..d61baef
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.der
Binary files differ
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.pem b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.pem
new file mode 100644
index 0000000..0e7b38e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAeGgAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDQ4JI1EJ239V4wss0jpVlZMudh2/kARCVdoBgsRQuvc2RNnO23Eyynlt9UN+Dc
+NRdQIhbCpVTjdEl/bePECHlqg9NE3frAj5GebiUdWL6A/idKsZA1nAKyIgxxjcnu
++38OcrlO6XOm36euxGfd/ULrghZGXzMVFq4uLiIv3DqFkUcIlE0BvUiUoNwpopV4
+MKj1GQgoaEObJG5xkMBKO6vg36VfJ3s3V3r48uJxYGhhBZEB0EpoXLd4i0piAB8S
+MLb0Ek6wA/HZ8A2rdnStk1wl/83OM1jO0uB3hyfJpqIijlvNGnrloYyyOIqS0LGH
+nxSJD7goASH2Ef0h4yxbsOvHAgMBAAGjUDBOMB0GA1UdDgQWBBQXAi1zEH84mzkS
+62ohswGGWSwdbzAfBgNVHSMEGDAWgBQXAi1zEH84mzkS62ohswGGWSwdbzAMBgNV
+HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB92T5toLkF6dLl65/boH5Qvub
+5wfIk0AD12T3t3kYWQFOH0YDCHNL3SfmrjYM/CwNJAd1KuCL5AZcn0km/n0SFXt5
+8Ps/MBcb0eK1fYezeEehKUyt5IBgDTKeQOel6So8rGuQRrDf/WV8rt6fugkIODFx
+sB3oj4ESaGXbvmvWD6q4a3koq/nV26kALchnAr7/FTNq3HEIQ1BDr9pldVh1gEV/
+ohHKcQP4M22Es7lredzpIcb5K6Ko/UtwsSRtHnoOjwmb+L/FsgAJsekmcJG5TK1X
+ciIsrrNFDCYzf/d9O1PD/V95kB7460qMzrGWZpc3mLe+OnmVMq6c4omOtIKl
+-----END CERTIFICATE-----