Merge "Implement the new v4 signing scheme in apksigner" into rvc-dev am: aa1210ed7e

Change-Id: Ie3ff27347b3de36778326fb8930b8d31d566350a
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index d2f8a1b..f4a948e 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -327,6 +327,7 @@
                         .setV2SigningEnabled(v2SigningEnabled)
                         .setV3SigningEnabled(v3SigningEnabled)
                         .setV4SigningEnabled(v4SigningEnabled)
+                        .setV4SigningRequested(v4SigningEnabled && v4SigningFlagFound)
                         .setDebuggableApkPermitted(debuggableApkPermitted)
                         .setSigningCertificateLineage(lineage);
         if (minSdkVersionSpecified) {
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 3b63744..d1e31f1 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -90,6 +90,7 @@
     private final boolean mV2SigningEnabled;
     private final boolean mV3SigningEnabled;
     private final boolean mV4SigningEnabled;
+    private final boolean mV4SigningRequested;
     private final boolean mDebuggableApkPermitted;
     private final boolean mOtherSignersSignaturesPreserved;
     private final String mCreatedBy;
@@ -115,6 +116,7 @@
             boolean v2SigningEnabled,
             boolean v3SigningEnabled,
             boolean v4SigningEnabled,
+            boolean v4SigningRequested,
             boolean debuggableApkPermitted,
             boolean otherSignersSignaturesPreserved,
             String createdBy,
@@ -134,6 +136,7 @@
         mV2SigningEnabled = v2SigningEnabled;
         mV3SigningEnabled = v3SigningEnabled;
         mV4SigningEnabled = v4SigningEnabled;
+        mV4SigningRequested = v4SigningRequested;
         mDebuggableApkPermitted = debuggableApkPermitted;
         mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
         mCreatedBy = createdBy;
@@ -572,7 +575,7 @@
 
         // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
         if (mV4SigningEnabled) {
-            signerEngine.signV4(outputApkIn, mOutputV4File);
+            signerEngine.signV4(outputApkIn, mOutputV4File, !mV4SigningRequested);
         }
     }
 
@@ -993,6 +996,7 @@
         private boolean mV2SigningEnabled = true;
         private boolean mV3SigningEnabled = true;
         private boolean mV4SigningEnabled = true;
+        private boolean mV4SigningRequested = false;
         private boolean mDebuggableApkPermitted = true;
         private boolean mOtherSignersSignaturesPreserved;
         private String mCreatedBy;
@@ -1279,6 +1283,15 @@
         }
 
         /**
+         * Explicitly request V4 signing. The tool will fail if V4 signing fails.
+         */
+        public Builder setV4SigningRequested(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV4SigningRequested = enabled;
+            return this;
+        }
+
+        /**
          * Sets whether the APK should be signed even if it is marked as debuggable ({@code
          * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
          * compatibility reasons, the default value of this setting is {@code true}.
@@ -1391,6 +1404,7 @@
                     mV2SigningEnabled,
                     mV3SigningEnabled,
                     mV4SigningEnabled,
+                    mV4SigningRequested,
                     mDebuggableApkPermitted,
                     mOtherSignersSignaturesPreserved,
                     mCreatedBy,
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 49a136b..c79f232 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -312,7 +312,8 @@
      * Generates a V4 signature proto and write to output file.
      *
      * @param data Input data to calculate a verity hash tree and hash root
-     * @param outputFile Serialized V4 Signature protobuf.
+     * @param outputFile To store the serialized V4 Signature.
+     * @param ignoreFailures Whether any failures will be silently ignored.
      * @throws InvalidKeyException if a signature could not be generated because a signing key is
      *         not suitable for generating the signature
      * @throws NoSuchAlgorithmException if a signature could not be generated because a required
@@ -320,7 +321,7 @@
      * @throws SignatureException if an error occurred while generating a signature
      * @throws IOException if protobuf fails to be serialized and written to file
      */
-    void signV4(DataSource data, File outputFile)
+    void signV4(DataSource data, File outputFile, boolean ignoreFailures)
             throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
 
     /**
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 76a8d74..dbe3034 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -2039,7 +2039,7 @@
          * </ul>
          */
         V4_SIG_UNKNOWN_SIG_ALGORITHM(
-                "V4 signature has unknown signing algorithm"),
+                "V4 signature has unknown signing algorithm: %1$#x"),
 
         /**
          * This APK Signature Scheme V4 signer offers no signatures.
@@ -2055,38 +2055,34 @@
                 "V4 signature has no supported signature"),
 
         /**
-         * PKCS7 signature block in the APK Signature Scheme V4 signature of this signer
-         * could not be parsed.
-         *
-         * <ul>
-         * <li>Parameter 1: error details ({@code Throwable})</li>
-         * </ul>
-         */
-        V4_SIG_MALFORMED_PKCS7(
-                "V4 signature has malformed pkcs7 signature block."),
-
-        /**
-         * APK Signature Scheme V4 signature over the signed data did not verify.
-         * The signed data includes hash root and v3 digest.
+         * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
          *
          * <ul>
          * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
          * </ul>
          */
-        V4_SIG_DID_NOT_VERIFY(
-                "V4 signature's pkcs7 signature does not verify"),
+        V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
 
         /**
-         * An exception was encountered while verifying APK Signature Scheme V4 signature
-         * of this signer.
+         * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+         * signer.
          *
          * <ul>
          * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
          * <li>Parameter 2: exception ({@code Throwable})</li>
          * </ul>
          */
-        V4_SIG_VERIFY_EXCEPTION(
-                "V4 signature cannot be verified"),
+        V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be
+         * parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
 
         /**
          * This APK Signature Scheme V4 signer's certificate could not be parsed.
@@ -2117,31 +2113,7 @@
          * </ul>
          */
         V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
-                "V4 signature has mismatched certificate and signature"),
-
-        /**
-         * Failed to parse this signer's digest record contained in the APK Signature Scheme
-         * V4 signature.
-         *
-         * <ul>
-         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
-         * </ul>
-         */
-        V4_SIG_MALFORMED_DIGEST(
-                "V4 signature has malformed content digest (hash tree root)"),
-
-        /**
-         * This APK Signature Scheme V4 signer's signature algorithms listed in the
-         * public key field of the signature proto do not match the signature algorithms listed in
-         * the signature field of the proto.
-         *
-         * <ul>
-         * <li>Parameter 1: signature algorithms from public key field ({@code List<Integer>})</li>
-         * <li>Parameter 2: signature algorithms from signature field ({@code List<Integer>})</li>
-         * </ul>
-         */
-        V4_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
-                "V4 signature has mismatched signature and digest"),
+                "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"),
 
         /**
          * The APK's hash root (aka digest) does not match the hash root contained in the Signature
@@ -2170,12 +2142,6 @@
                 "V4 signature's hash tree did not verity"),
 
         /**
-         * No signer found in v4 signature block
-         */
-        V4_SIG_NO_SIGNER(
-                "V4 signature has no signer"),
-
-        /**
          * Using more than one Signer to sign APK Signature Scheme V4 signature.
          */
         V4_SIG_MULTIPLE_SIGNERS(
@@ -2192,22 +2158,6 @@
                 "V4 signature and V3 signature have mismatched v3 digests"),
 
         /**
-         * The hash root value stored as one of the v4 signature fields does not match with the hash
-         * root value that is embedded as part of the pcks7's attached data.
-         */
-        V4_SIG_ROOT_HASH_MISMATCH_WITH_ATTACHED_DATA(
-                "V4 signature's root hash in the signature file does not match with the "
-                        + "root hash embedded in the pkcs7's attached data"),
-
-        /**
-         * The v3 digest value stored as one of the v4 signature fields does not match with the hash
-         * v3 digest value that is embedded as part of the pcks7's attached data.
-         */
-        V4_SIG_V3_DIGEST_MISMATCH_WITH_ATTACHED_DATA(
-                "V4 signature's v3 digest in the signature file does not match with the "
-                        + "v3 digest embedded in the pkcs7's attached data"),
-
-        /**
          * The v4 signature format version isn't the same as the tool's current version, something
          * may go wrong.
          */
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 0e0b554..788b78e 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -362,8 +362,7 @@
                 createSigningBlockSignerConfigs(
                         true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
         if (configs.size() != 1) {
-            throw new IllegalStateException(
-                    "Only accepting one signer config for V4 Signature Proto.");
+            throw new IllegalStateException("Only accepting one signer config for V4 Signature.");
         }
         return configs.get(0);
     }
@@ -438,10 +437,11 @@
             case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
                 try {
                     newSignerConfig.signatureAlgorithms =
-                            Collections.singletonList(
-                                    V4SchemeSigner.getSuggestedSignatureAlgorithm(publicKey));
+                            V4SchemeSigner.getSuggestedSignatureAlgorithms(publicKey,
+                                    mMinSdkVersion, apkSigningBlockPaddingSupported);
                 } catch (InvalidKeyException e) {
-                    newSignerConfig.signatureAlgorithms = new ArrayList<>();
+                    // V4 is an optional signing schema, ok to proceed without.
+                    newSignerConfig.signatureAlgorithms = null;
                 }
                 break;
             case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
@@ -896,15 +896,22 @@
     }
 
     @Override
-    public void signV4(DataSource dataSource, File outputFile) {
+    public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+            throws SignatureException {
         if (outputFile == null) {
-            return;
+            if (ignoreFailures) {
+                return;
+            }
+            throw new SignatureException("Missing V4 output file.");
         }
         try {
             ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
             V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
-        } catch (InvalidKeyException | IOException | NoSuchAlgorithmException ignored) {
-            // It is okay to fail v4 signing for now.
+        } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+            if (ignoreFailures) {
+                return;
+            }
+            throw new SignatureException("V4 signing failed", e);
         }
     }
 
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 944b9b8..f7b34f0 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.apksig.internal.apk;
 
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;
+
 import com.android.apksig.ApkVerifier;
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.apk.ApkFormatException;
@@ -204,7 +206,7 @@
                             centralDir,
                             new ByteBufferDataSource(modifiedEocd));
             // Special checks for the verity algorithm requirements.
-            if (actualContentDigests.containsKey(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+            if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) {
                 if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
                     throw new RuntimeException(
                             "APK Signing Block is not aligned on 4k boundary: " +
@@ -444,7 +446,7 @@
                 new DataSource[] { beforeCentralDir, centralDir, eocd },
                 contentDigests);
 
-        if (digestAlgorithms.contains(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+        if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
             computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
         }
         return contentDigests;
@@ -765,16 +767,16 @@
         byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd);
         encoded.put(rootHash);
         encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
-        outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array());
+        outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array());
     }
 
     private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) {
         // FORMAT:
         // OFFSET       DATA TYPE  DESCRIPTION
         // * @+0  bytes uint8[32]  Merkle tree root hash of SHA-256
-        // * @+32 bytes int64      Length of source data (Optional)
+        // * @+32 bytes int64      (optional) Length of source data
         int backBufferSize =
-                ContentDigestAlgorithm.VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
+                VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
         if (includeSourceDataSize) {
             backBufferSize += Long.SIZE / Byte.SIZE;
         }
@@ -783,18 +785,29 @@
         return encoded;
     }
 
-    public static void computeChunkVerityTreeAndDigest(DataSource dataSource,
-            Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> outputHashTreeAndDigests)
+    public static class VerityTreeAndDigest {
+        public final ContentDigestAlgorithm contentDigestAlgorithm;
+        public final byte[] rootHash;
+        public final byte[] tree;
+
+        VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash,
+                byte[] tree) {
+            this.contentDigestAlgorithm = contentDigestAlgorithm;
+            this.rootHash = rootHash;
+            this.tree = tree;
+        }
+    }
+
+    public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
             throws IOException, NoSuchAlgorithmException {
         ByteBuffer encoded = createVerityDigestBuffer(false);
         // Use 0s as salt for now.  This also needs to be consistent in the fsverify header for
         // kernel to use.
         VerityTreeBuilder builder = new VerityTreeBuilder(null);
-        ByteBuffer tree = builder.generateVerityTree(dataSource, false /* padding */);
+        ByteBuffer tree = builder.generateVerityTree(dataSource);
         byte[] rootHash = builder.getRootHashFromTree(tree);
         encoded.put(rootHash);
-        outputHashTreeAndDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
-                Pair.of(tree.array(), encoded.array()));
+        return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array());
     }
 
     private static long getChunkCount(long inputSize, long chunkSize) {
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 866402f..2475fae 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -18,11 +18,6 @@
 
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
 import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
-import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
-import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
-import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
-import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
-import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
 
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
@@ -31,81 +26,66 @@
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
 import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
-import com.android.apksig.internal.asn1.Asn1EncodingException;
-import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.zip.ZipFormatException;
 
-import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
-import java.security.interfaces.ECKey;
-import java.security.interfaces.RSAKey;
-import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 /**
- * APK Signature Scheme V4 signer.
- * V4 scheme file contains 3 mandatory fields - used during installation.
- * And optional verity tree - has to be present during session commit.
- *
+ * APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
+ * installation. And optional verity tree - has to be present during session commit.
+ * <p>
  * The fields:
  * <p>
- * 1. verityRootHash: bytes of the hash tree root (digest of first 1-page of the tree),
- * 2. V3Digest: digest from v2/v3 signing schema,
- * 3. pkcs7SignatureBlock: bytes of the signature over encoded signed data, which includes 1 and 2,
+ * 1. hashingInfo - verity root hash and hashing info,
+ * 2. signingInfo - certificate, public key and signature,
+ * For more details see V4Signature.
  * </p>
  * (optional) verityTree: integer size prepended bytes of the verity hash tree.
- *
- * TODO(schfan): Pass v3 digest to v4 signature proto and add verification code
- * TODO(schfan): Add v4 unit tests
+ * <p>
+ * TODO(schfan): Pass v3 digest to v4 signature proto and add verification code TODO(schfan): Add v4
+ * unit tests
  */
 public abstract class V4SchemeSigner {
-    /** Hidden constructor to prevent instantiation. */
+    /**
+     * Hidden constructor to prevent instantiation.
+     */
     private V4SchemeSigner() {
     }
 
     /**
      * Based on a public key, return a signing algorithm that supports verity.
      */
-    public static SignatureAlgorithm getSuggestedSignatureAlgorithm(PublicKey signingKey)
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean apkSigningBlockPaddingSupported)
             throws InvalidKeyException {
-        final String keyAlgorithm = signingKey.getAlgorithm();
-        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-            final int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
-            if (modulusLengthBits <= 3072) {
-                return SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
-            } else {
-                // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
-                // digest being the weak link. SHA-512 is the next strongest supported digest.
-                throw new InvalidKeyException(
-                        "Key requires SHA-512 signature algorithm, not yet supported with verity");
+        List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+                signingKey, minSdkVersion,
+                apkSigningBlockPaddingSupported);
+        // Keeping only supported algorithms.
+        for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
+            final SignatureAlgorithm algorithm = iter.next();
+            if (!isSupported(algorithm.getContentDigestAlgorithm())) {
+                iter.remove();
             }
-        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
-            return SignatureAlgorithm.VERITY_DSA_WITH_SHA256;
-        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-            final int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
-            if (keySizeBits <= 256) {
-                return SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256;
-            } else {
-                throw new InvalidKeyException(
-                        "Key requires SHA-512 signature algorithm, not yet supported with verity");
-            }
-        } else {
-            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
         }
+        return algorithms;
     }
 
     /**
@@ -116,9 +96,14 @@
             SignerConfig signerConfig,
             File outputFile)
             throws IOException, InvalidKeyException, NoSuchAlgorithmException {
-        Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> verityDigest = new HashMap<>();
-        ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent, verityDigest);
+        // Salt has to stay empty for fs-verity compatibility.
+        final byte[] salt = null;
+        // Not used by apksigner.
+        final byte[] additionalData = null;
 
+        final long fileSize = apkContent.size();
+
+        // Obtaining first supported digest from v3 block (SHA256 or SHA512).
         byte[] v3digest;
         try {
             v3digest = getV3Digest(apkContent);
@@ -127,79 +112,83 @@
             throw new IOException("Failed to parse V3-signed apk to read its V3 digest");
         }
 
-        final Pair<V4Signature, byte[]> signaturePair;
+        // Obtaining the merkle tree and the root hash in verity format.
+        ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
+                ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+        final ContentDigestAlgorithm verityContentDigestAlgorithm =
+                verityContentDigestInfo.contentDigestAlgorithm;
+        final byte[] rootHash = verityContentDigestInfo.rootHash;
+        final byte[] tree = verityContentDigestInfo.tree;
+
+        final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
+                verityContentDigestAlgorithm);
+        final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
+                hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
+                salt, rootHash);
+
+        // Generating SigningInfo and combining everything into V4Signature.
+        final V4Signature signature;
         try {
-            signaturePair = generateSignatureObject(signerConfig, verityDigest, v3digest);
-        } catch (InvalidKeyException | SignatureException |
-                CertificateEncodingException | Asn1EncodingException e) {
+            signature = generateSignature(signerConfig, hashingInfo, v3digest, additionalData,
+                    fileSize);
+        } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
             throw new InvalidKeyException("Signer failed", e);
         }
 
-        V4Signature signature = signaturePair.getFirst();
-        byte[] tree = signaturePair.getSecond();
-
-        try (final DataOutputStream output = new DataOutputStream(
-                new FileOutputStream(outputFile))) {
+        try (final OutputStream output = new FileOutputStream(outputFile)) {
             signature.writeTo(output);
-            if (tree != null && tree.length != 0) {
-                V4Signature.writeBytes(output, tree);
-            }
+            V4Signature.writeBytes(output, tree);
+        } catch (IOException e) {
+            outputFile.delete();
+            throw e;
         }
     }
 
-    private static Pair<V4Signature, byte[]> generateSignatureObject(
+    private static V4Signature generateSignature(
             SignerConfig signerConfig,
-            Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> contentDigests,
-            byte[] v3Digest)
+            V4Signature.HashingInfo hashingInfo,
+            byte[] v3Digest, byte[] additionaData, long fileSize)
             throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
-            CertificateEncodingException, Asn1EncodingException {
+            CertificateEncodingException {
         if (signerConfig.certificates.isEmpty()) {
             throw new SignatureException("No certificates configured for signer");
         }
-        final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
-        final List<byte[]> certificates = encodeCertificates(signerConfig.certificates);
-        if (certificates.size() != 1) {
+        if (signerConfig.certificates.size() != 1) {
             throw new CertificateEncodingException("Should only have one certificate");
         }
 
-        if (signerConfig.signatureAlgorithms.size() != 1) {
-            throw new SignatureException("Should only be one signature algorithm");
-        }
+        // Collecting data for signing.
+        final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
 
-        final SignatureAlgorithm signatureAlgorithm = signerConfig.signatureAlgorithms.get(0);
-        final ContentDigestAlgorithm contentDigestAlgorithm =
-                signatureAlgorithm.getContentDigestAlgorithm();
-        final Pair<byte[], byte[]> contentDigest = contentDigests.get(contentDigestAlgorithm);
-        if (contentDigest == null) {
-            throw new SignatureException("Cannot find computed digest");
-        }
-        byte[] tree = contentDigest.getFirst();
-        byte[] rootHash = contentDigest.getSecond();
+        final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
+        final byte[] encodedCertificate = encodedCertificates.get(0);
 
-        byte[] data = ByteBuffer.allocate(rootHash.length + v3Digest.length)
-                .put(rootHash).put(v3Digest).array();
+        final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(v3Digest,
+                encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
 
+        final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
+                signingInfoNoSignature);
+
+        // Signing.
         final List<Pair<Integer, byte[]>> signatures =
                 ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
         if (signatures.size() != 1) {
             throw new SignatureException("Should only be one signature generated");
         }
 
-        byte[] pkcs7SignatureBlock = ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
-                signatures.get(0).getSecond(), /* signature bytes */
-                ByteBuffer.wrap(data),
-                signerConfig.certificates,
-                new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL), /* digest algo id */
-                getSignatureAlgorithmIdentifier(publicKey));
+        final int signatureAlgorithmId = signatures.get(0).getFirst();
+        final byte[] signature = signatures.get(0).getSecond();
 
-        final V4Signature signature =
-                new V4Signature(V4Signature.CURRENT_VERSION,
-                        rootHash, v3Digest, pkcs7SignatureBlock);
-        return Pair.of(signature, tree);
+        final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(v3Digest,
+                encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
+                signature);
+
+        return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
+                signingInfo.toByteArray());
     }
 
-    // Get V3 digest by parsing the V3-signed apk
+    // Get V3 digest by parsing the V3-signed apk and choosing the first digest of supported type.
     private static byte[] getV3Digest(DataSource apk) throws ApkFormatException, IOException,
             ApkSigningBlockUtils.SignatureNotFoundException, NoSuchAlgorithmException,
             SignatureException {
@@ -227,40 +216,49 @@
         if (contentDigests.isEmpty()) {
             throw new SignatureException("Should have at least one digest");
         }
+
+        int bestAlgorithm = -1;
+        byte[] bestDigest = null;
         for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
             final SignatureAlgorithm signatureAlgorithm =
                     SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
-            if (signatureAlgorithm == null) {
-                continue;
-            }
             final ContentDigestAlgorithm contentDigestAlgorithm =
                     signatureAlgorithm.getContentDigestAlgorithm();
-            if (contentDigestAlgorithm == null) {
+            if (!isSupported(contentDigestAlgorithm)) {
                 continue;
             }
-            if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
-                || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
-                return contentDigest.getValue();
+            // Prefer SHA512 over SHA256, if both are available.
+            if (bestAlgorithm < contentDigestAlgorithm.getId()) {
+                bestAlgorithm = contentDigestAlgorithm.getId();
+                bestDigest = contentDigest.getValue();
             }
         }
-        throw new SignatureException("Failed to find any V3 digest in the source APK");
+        if (bestDigest == null) {
+            throw new SignatureException("Failed to find a supported V3 digest in the source APK");
+        }
+        return bestDigest;
     }
 
-    /**
-     * Returns the JCA SHA256 {@code AlgorithmIdentifier} and PKCS #7 {@code SignatureAlgorithm} to
-     * use when signing with the specified key.
-     */
-    private static AlgorithmIdentifier getSignatureAlgorithmIdentifier(
-            PublicKey publicKey) throws InvalidKeyException {
-        String keyAlgorithm = publicKey.getAlgorithm();
-        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-            return new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL);
-        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
-            return new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
-        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-            return new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL);
-        } else {
-            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+    private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm) {
+        if (contentDigestAlgorithm == null) {
+            return false;
+        }
+        if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+                || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
+            return true;
+        }
+        return false;
+    }
+
+    private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
+            throws NoSuchAlgorithmException {
+        switch (algorithm) {
+            case VERITY_CHUNKED_SHA256:
+                return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
+                        V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
+            default:
+                throw new NoSuchAlgorithmException(
+                        "Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
         }
     }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
index 96294da..18f9202 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -17,73 +17,61 @@
 package com.android.apksig.internal.apk.v4;
 
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
-import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm;
-import static com.android.apksig.internal.x509.Certificate.findCertificate;
-import static com.android.apksig.internal.x509.Certificate.parseCertificates;
 
 import com.android.apksig.ApkVerifier;
 import com.android.apksig.ApkVerifier.Issue;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.asn1.Asn1BerParser;
-import com.android.apksig.internal.asn1.Asn1DecodingException;
-import com.android.apksig.internal.pkcs7.ContentInfo;
-import com.android.apksig.internal.pkcs7.Pkcs7Constants;
-import com.android.apksig.internal.pkcs7.SignedData;
-import com.android.apksig.internal.pkcs7.SignerInfo;
-import com.android.apksig.internal.util.ByteBufferUtils;
-import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.util.DataSource;
 
-import java.io.DataInputStream;
-import java.io.EOFException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
+import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
 import java.security.Signature;
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
 import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 
 /**
  * APK Signature Scheme V4 verifier.
- *
+ * <p>
  * Verifies the serialized V4Signature file against an APK.
  */
 public abstract class V4SchemeVerifier {
-    /** Hidden constructor to prevent instantiation. */
+    /**
+     * Hidden constructor to prevent instantiation.
+     */
     private V4SchemeVerifier() {
     }
 
     /**
      * <p>
-     * The main goals of the verifier are:
-     * 1) parse V4Signature file fields
-     * 2) verifies the PKCS7 signature block against the raw root hash bytes in the proto field
-     * 3) verifies that the raw root hash matches with the actual hash tree root of the give APK
-     * 4) if the file contains a verity tree, verifies that it matches with the actual verity
-     * tree computed from the given APK.
+     * The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
+     * signature block against the raw root hash bytes in the proto field 3) verifies that the raw
+     * root hash matches with the actual hash tree root of the give APK 4) if the file contains a
+     * verity tree, verifies that it matches with the actual verity tree computed from the given
+     * APK.
      * </p>
      */
     public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
             throws IOException, NoSuchAlgorithmException {
-
-        V4Signature signature = null;
-        byte[] tree = null;
-        try (final DataInputStream input = new DataInputStream(
-                new FileInputStream(v4SignatureFile))) {
+        final V4Signature signature;
+        final byte[] tree;
+        try (InputStream input = new FileInputStream(v4SignatureFile)) {
             signature = V4Signature.readFrom(input);
             tree = V4Signature.readBytes(input);
-        } catch (EOFException e) {
         }
 
         final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
@@ -100,203 +88,163 @@
                     V4Signature.CURRENT_VERSION);
         }
 
-        final byte[] pkcs7Signature = signature.pkcs7SignatureBlock;
-        final ByteBuffer pkcs7SignatureBlock =
-                ByteBuffer.wrap(pkcs7Signature).order(ByteOrder.LITTLE_ENDIAN);
-        final ByteBuffer verityRootHash = ByteBuffer.wrap(signature.verityRootHash);
-        final ByteBuffer v3Digest = ByteBuffer.wrap(signature.v3Digest);
+        V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
+                signature.hashingInfo);
+        V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+                signature.signingInfo);
 
-        result.signers.add(parseAndVerifySignatureBlock(
-                pkcs7SignatureBlock, verityRootHash, v3Digest));
+        final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
+
+        // First, verify the signature over signedData.
+        ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
+                signingInfo, signedData);
+        result.signers.add(signerInfo);
         if (result.containsErrors()) {
             return result;
         }
 
-        verifyRootHashAndTree(apk, result, signature.verityRootHash, tree);
+        // Second, check if the root hash and the tree are correct.
+        verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
         if (!result.containsErrors()) {
             result.verified = true;
         }
-        // Add v3Content digest from the file to the result
-        result.signers.get(0).contentDigests.add(
-                new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
-                        0 /* signature algorithm id doesn't matter here */,
-                        signature.v3Digest));
+
         return result;
     }
 
     /**
-     * Parses the provided pkcs7 signature block and populates the {@code result}.
-     *
-     * This verifies {@pkcs7SignatureBlock} over {@code verityRootHash}, as well as
-     * parsing the certificate contained in the signature block.
-     * This method adds one or more errors to the {@code result}.
+     * Parses the provided signature block and populates the {@code result}.
+     * <p>
+     * This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
+     * contained in the signature block. This method adds one or more errors to the {@code result}.
      */
     private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
-            ByteBuffer pkcs7SignatureBlock,
-            ByteBuffer verityRootHash, ByteBuffer v3Digest) {
+            V4Signature.SigningInfo signingInfo,
+            final byte[] signedData) throws NoSuchAlgorithmException {
         final ApkSigningBlockUtils.Result.SignerInfo result =
                 new ApkSigningBlockUtils.Result.SignerInfo();
-        SignedData signedData;
+        result.index = 0;
+
+        final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
+        final byte[] sigBytes = signingInfo.signature;
+        result.signatures.add(
+                new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
+
+        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+        if (signatureAlgorithm == null) {
+            result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+            return result;
+        }
+
+        String jcaSignatureAlgorithm =
+                signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+        AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+
+        String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+
+        final byte[] publicKeyBytes = signingInfo.publicKey;
+        PublicKey publicKey;
         try {
-            final ContentInfo contentInfo =
-                    Asn1BerParser.parse(pkcs7SignatureBlock, ContentInfo.class);
-            if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) {
-                result.addError(Issue.V4_SIG_MALFORMED_PKCS7,
-                        "Unsupported ContentInfo.contentType: "
-                                + contentInfo.contentType);
+            publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
+                    new X509EncodedKeySpec(publicKeyBytes));
+        } catch (Exception e) {
+            result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
+            return result;
+        }
+
+        try {
+            Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+            sig.initVerify(publicKey);
+            if (jcaSignatureAlgorithmParams != null) {
+                sig.setParameter(jcaSignatureAlgorithmParams);
             }
-            signedData = Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class);
-        } catch (Asn1DecodingException e) {
-            result.addError(Issue.V4_SIG_MALFORMED_PKCS7, e);
+            sig.update(signedData);
+            if (!sig.verify(sigBytes)) {
+                result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+                return result;
+            }
+            result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException
+                | SignatureException e) {
+            result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
             return result;
         }
 
-        if (signedData.signerInfos.isEmpty()) {
-            result.addError(Issue.V4_SIG_NO_SIGNER);
+        if (signingInfo.certificate == null) {
+            result.addError(Issue.V4_SIG_NO_CERTIFICATE);
             return result;
         }
 
-        if (signedData.signerInfos.size() != 1) {
-            result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
-            return result;
-        }
-
-        ByteBuffer attachedData = signedData.encapContentInfo.content;
-        byte[] rootHashInAttachedData = new byte[verityRootHash.array().length];
-        attachedData.get(rootHashInAttachedData);
-        // Embedded root hash should be equal to the external one
-        if (!Arrays.equals(rootHashInAttachedData, verityRootHash.array())) {
-            result.addError(Issue.V4_SIG_ROOT_HASH_MISMATCH_WITH_ATTACHED_DATA);
-            return result;
-        }
-        byte[] v3digestInAttachedData = new byte[v3Digest.array().length];
-        attachedData.get(v3digestInAttachedData);
-        // Embedded root hash should be equal to the external one
-        if (!Arrays.equals(v3digestInAttachedData, v3Digest.array())) {
-            result.addError(Issue.V4_SIG_V3_DIGEST_MISMATCH_WITH_ATTACHED_DATA);
-            return result;
-        }
-        attachedData.flip();
-
-        SignerInfo unverifiedSignerInfo = signedData.signerInfos.get(0);
-        List<X509Certificate> signedDataCertificates;
+        final X509Certificate certificate;
         try {
-            signedDataCertificates = parseCertificates(signedData.certificates);
+            // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+            // form. Without this, getEncoded may return a different form from what was stored in
+            // the signature. This is because some X509Certificate(Factory) implementations
+            // re-encode certificates.
+            certificate = new GuaranteedEncodedFormX509Certificate(
+                    X509CertificateUtils.generateCertificate(signingInfo.certificate),
+                    signingInfo.certificate);
         } catch (CertificateException e) {
             result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
             return result;
         }
+        result.certs.add(certificate);
 
-        // Verify SignerInfo
-        verifySignerInfo(signedDataCertificates, unverifiedSignerInfo, attachedData, result);
+        byte[] certificatePublicKeyBytes;
+        try {
+            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+                    certificate.getPublicKey());
+        } catch (InvalidKeyException e) {
+            System.out.println("Caught an exception encoding the public key: " + e);
+            e.printStackTrace();
+            certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
+        }
+        if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+            result.addError(
+                    Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+                    ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+                    ApkSigningBlockUtils.toHex(publicKeyBytes));
+            return result;
+        }
+
+        // Add v3Content digest from the file to the result.
+        ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
+                new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+                        0 /* signature algorithm id doesn't matter here */, signingInfo.v3Digest);
+        result.contentDigests.add(contentDigest);
+
         return result;
     }
 
-    private static void verifySignerInfo(
-            List<X509Certificate> signedDataCertificates, SignerInfo signerInfo,
-            ByteBuffer data, ApkSigningBlockUtils.Result.SignerInfo result) {
-        final String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm;
-        final String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm;
-        final X509Certificate signingCertificate =
-                findCertificate(signedDataCertificates, signerInfo.sid);
-        result.certs.clear();
-        result.certs.add(signingCertificate);
-        if (signingCertificate == null) {
-            result.addError(Issue.V4_SIG_NO_CERTIFICATE);
-            return;
-        }
-        // Check whether the signing certificate is acceptable. Android performs these
-        // checks explicitly, instead of delegating this to
-        // Signature.initVerify(Certificate).
-        if (signingCertificate.hasUnsupportedCriticalExtension()) {
-            result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE,
-                    "Signing certificate has unsupported critical extensions");
-            return;
-        }
-        final boolean[] keyUsageExtension = signingCertificate.getKeyUsage();
-        if (keyUsageExtension != null) {
-            boolean digitalSignature = (keyUsageExtension.length >= 1) && keyUsageExtension[0];
-            boolean nonRepudiation = (keyUsageExtension.length >= 2) && keyUsageExtension[1];
-            if (!digitalSignature && !nonRepudiation) {
-                result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE,
-                        "Signing certificate not authorized for use in digital signatures"
-                                + ": keyUsage extension missing digitalSignature and"
-                                + " nonRepudiation");
-                return;
-            }
-        }
-
-        Signature s;
-        try {
-            final String jcaSignatureAlgorithm = getJcaSignatureAlgorithm(
-                    digestAlgorithmOid, signatureAlgorithmOid);
-            s = Signature.getInstance(jcaSignatureAlgorithm);
-        } catch (SignatureException | NoSuchAlgorithmException e) {
-            result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM);
-            return;
-        }
-        if (signerInfo.signedAttrs != null) {
-            result.addError(Issue.V4_SIG_MALFORMED_SIGNERS, "Should not contain signed attributes");
-        }
-        try {
-            s.initVerify(signingCertificate.getPublicKey());
-            s.update(data);
-            final byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice());
-            if (!s.verify(sigBytes)) {
-                result.addError(Issue.V4_SIG_DID_NOT_VERIFY);
-            }
-        } catch (InvalidKeyException | SignatureException e) {
-            result.addError(Issue.V4_SIG_VERIFY_EXCEPTION);
-        }
-    }
-
     private static void verifyRootHashAndTree(DataSource apkContent,
-            ApkSigningBlockUtils.Result result, byte[] rootHashInResult, byte[] treeInResult)
-            throws IOException, NoSuchAlgorithmException {
-        final Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> actualContentDigests =
-                new HashMap<>();
-        ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent, actualContentDigests);
-        if (result.signers.size() != 1) {
-            throw new IllegalStateException("There should only be one signer for V4");
+            ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
+            byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
+        ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
+                ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+        ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
+        final byte[] actualDigest = actualContentDigestInfo.rootHash;
+        final byte[] actualTree = actualContentDigestInfo.tree;
+
+        if (!Arrays.equals(expectedDigest, actualDigest)) {
+            signerInfo.addError(
+                    ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
+                    algorithm,
+                    toHex(expectedDigest),
+                    toHex(actualDigest));
+            return;
         }
-        final ApkSigningBlockUtils.Result.SignerInfo signerInfo = result.signers.get(0);
-        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest expected
-                : signerInfo.contentDigests) {
-            final SignatureAlgorithm signatureAlgorithm =
-                    SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
-            if (signatureAlgorithm == null) {
-                continue;
-            }
-            final ContentDigestAlgorithm contentDigestAlgorithm =
-                    signatureAlgorithm.getContentDigestAlgorithm();
-            // should not happen
-            if (contentDigestAlgorithm != ContentDigestAlgorithm.VERITY_CHUNKED_SHA256) {
-                continue;
-            }
-            final byte[] expectedDigest = expected.getValue();
-            final byte[] actualDigest =
-                    actualContentDigests.get(contentDigestAlgorithm).getSecond();
-            final byte[] actualTree =
-                    actualContentDigests.get(contentDigestAlgorithm).getFirst();
-            if (!Arrays.equals(expectedDigest, actualDigest)
-                    || !Arrays.equals(expectedDigest, rootHashInResult)) {
-                signerInfo.addError(
-                        ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
-                        contentDigestAlgorithm,
-                        toHex(expectedDigest),
-                        toHex(actualDigest));
-                continue;
-            }
-            // Only check verity tree if it is not empty
-            if (treeInResult != null && !Arrays.equals(treeInResult, actualTree)) {
-                signerInfo.addError(
-                        ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
-                        contentDigestAlgorithm,
-                        toHex(expectedDigest),
-                        toHex(actualDigest));
-                continue;
-            }
-            signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
+        // Only check verity tree if it is not empty
+        if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
+            signerInfo.addError(
+                    ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
+                    algorithm,
+                    toHex(expectedDigest),
+                    toHex(actualDigest));
+            return;
         }
+
+        signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
     }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
index 2963cd1..0aefade 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -16,48 +16,210 @@
 
 package com.android.apksig.internal.apk.v4;
 
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
+import java.io.EOFException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 public class V4Signature {
-    public static final int CURRENT_VERSION = 1;
+    public static final int CURRENT_VERSION = 2;
 
-    public final int version;
-    public final byte[] verityRootHash;
-    public final byte[] v3Digest;
-    public final byte[] pkcs7SignatureBlock;
+    public static final int HASHING_ALGORITHM_SHA256 = 1;
+    public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
 
-    V4Signature(int version, byte[] verityRootHash, byte[] v3Digest, byte[] pkcs7SignatureBlock) {
+    public static class HashingInfo {
+        public final int hashAlgorithm; // only 1 == SHA256 supported
+        public final byte log2BlockSize; // only 12 (block size 4096) supported now
+        public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
+        public final byte[] rawRootHash; // salted digest of the first Merkle tree page
+
+        HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
+            this.hashAlgorithm = hashAlgorithm;
+            this.log2BlockSize = log2BlockSize;
+            this.salt = salt;
+            this.rawRootHash = rawRootHash;
+        }
+
+        static HashingInfo fromByteArray(byte[] bytes) throws IOException {
+            ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+            final int hashAlgorithm = buffer.getInt();
+            final byte log2BlockSize = buffer.get();
+            byte[] salt = readBytes(buffer);
+            byte[] rawRootHash = readBytes(buffer);
+            return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
+        }
+
+        byte[] toByteArray() {
+            final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
+                    + bytesSize(this.rawRootHash);
+            ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+            buffer.putInt(this.hashAlgorithm);
+            buffer.put(this.log2BlockSize);
+            writeBytes(buffer, this.salt);
+            writeBytes(buffer, this.rawRootHash);
+            return buffer.array();
+        }
+    }
+
+    public static class SigningInfo {
+        public final byte[] v3Digest;  // used to match with the corresponding APK
+        public final byte[] certificate; // ASN.1 DER form
+        public final byte[] additionalData; // a free-form binary data blob
+        public final byte[] publicKey; // ASN.1 DER, must match the certificate
+        public final int signatureAlgorithmId; // see the APK v2 doc for the list
+        public final byte[] signature;
+
+        SigningInfo(byte[] v3Digest, byte[] certificate, byte[] additionalData,
+                byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
+            this.v3Digest = v3Digest;
+            this.certificate = certificate;
+            this.additionalData = additionalData;
+            this.publicKey = publicKey;
+            this.signatureAlgorithmId = signatureAlgorithmId;
+            this.signature = signature;
+        }
+
+        static SigningInfo fromByteArray(byte[] bytes) throws IOException {
+            ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+            byte[] v3Digest = readBytes(buffer);
+            byte[] certificate = readBytes(buffer);
+            byte[] additionalData = readBytes(buffer);
+            byte[] publicKey = readBytes(buffer);
+            int signatureAlgorithmId = buffer.getInt();
+            byte[] signature = readBytes(buffer);
+            return new SigningInfo(v3Digest, certificate, additionalData, publicKey,
+                    signatureAlgorithmId, signature);
+        }
+
+        byte[] toByteArray() {
+            final int size = bytesSize(this.v3Digest) + bytesSize(this.certificate) + bytesSize(
+                    this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
+                    + bytesSize(this.signature);
+            ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+            writeBytes(buffer, this.v3Digest);
+            writeBytes(buffer, this.certificate);
+            writeBytes(buffer, this.additionalData);
+            writeBytes(buffer, this.publicKey);
+            buffer.putInt(this.signatureAlgorithmId);
+            writeBytes(buffer, this.signature);
+            return buffer.array();
+        }
+    }
+
+    public final int version; // Always 2 for now.
+    public final byte[] hashingInfo;
+    public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
+
+    V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
         this.version = version;
-        this.verityRootHash = verityRootHash;
-        this.v3Digest = v3Digest;
-        this.pkcs7SignatureBlock = pkcs7SignatureBlock;
+        this.hashingInfo = hashingInfo;
+        this.signingInfo = signingInfo;
     }
 
-    static byte[] readBytes(DataInputStream stream) throws IOException {
-        byte[] result = new byte[stream.readInt()];
-        stream.read(result);
-        return result;
+    static V4Signature readFrom(InputStream stream) throws IOException {
+        final int version = readIntLE(stream);
+        if (version != CURRENT_VERSION) {
+            throw new IOException("Invalid signature version.");
+        }
+        final byte[] hashingInfo = readBytes(stream);
+        final byte[] signingInfo = readBytes(stream);
+        return new V4Signature(version, hashingInfo, signingInfo);
     }
 
-    static V4Signature readFrom(DataInputStream stream) throws IOException {
-        final int version = stream.readInt();
-        byte[] verityRootHash = readBytes(stream);
-        byte[] v3Digest = readBytes(stream);
-        byte[] pkcs7SignatureBlock = readBytes(stream);
-        return new V4Signature(version, verityRootHash, v3Digest, pkcs7SignatureBlock);
+    void writeTo(OutputStream stream) throws IOException {
+        writeIntLE(stream, this.version);
+        writeBytes(stream, this.hashingInfo);
+        writeBytes(stream, this.signingInfo);
     }
 
-    static void writeBytes(DataOutputStream stream, byte[] bytes) throws IOException {
-        stream.writeInt(bytes.length);
+    static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
+        final int size =
+                4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
+                        hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
+                        signingInfo.v3Digest) + bytesSize(signingInfo.certificate) + bytesSize(
+                        signingInfo.additionalData);
+        ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putInt(size);
+        buffer.putLong(fileSize);
+        buffer.putInt(hashingInfo.hashAlgorithm);
+        buffer.put(hashingInfo.log2BlockSize);
+        writeBytes(buffer, hashingInfo.salt);
+        writeBytes(buffer, hashingInfo.rawRootHash);
+        writeBytes(buffer, signingInfo.v3Digest);
+        writeBytes(buffer, signingInfo.certificate);
+        writeBytes(buffer, signingInfo.additionalData);
+        return buffer.array();
+    }
+
+    // Utility methods.
+    static int bytesSize(byte[] bytes) {
+        return 4/*length*/ + (bytes == null ? 0 : bytes.length);
+    }
+
+    static void readFully(InputStream stream, byte[] buffer) throws IOException {
+        int len = buffer.length;
+        int n = 0;
+        while (n < len) {
+            int count = stream.read(buffer, n, len - n);
+            if (count < 0) {
+                throw new EOFException();
+            }
+            n += count;
+        }
+    }
+
+    static int readIntLE(InputStream stream) throws IOException {
+        final byte[] buffer = new byte[4];
+        readFully(stream, buffer);
+        return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
+    }
+
+    static void writeIntLE(OutputStream stream, int v) throws IOException {
+        final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
+        stream.write(buffer);
+    }
+
+    static byte[] readBytes(InputStream stream) throws IOException {
+        try {
+            final int size = readIntLE(stream);
+            final byte[] bytes = new byte[size];
+            readFully(stream, bytes);
+            return bytes;
+        } catch (EOFException ignored) {
+            return null;
+        }
+    }
+
+    static byte[] readBytes(ByteBuffer buffer) throws IOException {
+        if (buffer.remaining() < 4) {
+            throw new EOFException();
+        }
+        final int size = buffer.getInt();
+        if (buffer.remaining() < size) {
+            throw new EOFException();
+        }
+        final byte[] bytes = new byte[size];
+        buffer.get(bytes);
+        return bytes;
+    }
+
+    static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
+        if (bytes == null) {
+            writeIntLE(stream, 0);
+            return;
+        }
+        writeIntLE(stream, bytes.length);
         stream.write(bytes);
     }
 
-    void writeTo(DataOutputStream stream) throws IOException {
-        stream.writeInt(this.version);
-        writeBytes(stream, this.verityRootHash);
-        writeBytes(stream, this.v3Digest);
-        writeBytes(stream, this.pkcs7SignatureBlock);
+    static void writeBytes(ByteBuffer buffer, byte[] bytes) {
+        if (bytes == null) {
+            buffer.putInt(0);
+            return;
+        }
+        buffer.putInt(bytes.length);
+        buffer.put(bytes);
     }
 }
diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
index 3133606..dc048f0 100644
--- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
+++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -82,9 +82,8 @@
     /**
      * Returns the root hash of the verity tree built from the data source.
      */
-    public byte[] generateVerityTreeRootHash(DataSource fileSource)
-            throws IOException {
-        ByteBuffer verityBuffer = generateVerityTree(fileSource, true /* addPadding */);
+    public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+        ByteBuffer verityBuffer = generateVerityTree(fileSource);
         return getRootHashFromTree(verityBuffer);
     }
 
@@ -101,8 +100,7 @@
      * The tree is currently stored only in memory and is never written out.  Nevertheless, it is
      * the actual verity tree format on disk, and is supposed to be re-generated on device.
      */
-    public ByteBuffer generateVerityTree(DataSource fileSource, boolean addPadding)
-            throws IOException {
+    public ByteBuffer generateVerityTree(DataSource fileSource) throws IOException {
         int digestSize = mMd.getDigestLength();
 
         // Calculate the summed area table of level size. In other word, this is the offset
@@ -118,11 +116,11 @@
             DataSource src;
             if (i == levelOffset.length - 2) {
                 src = fileSource;
-                digestDataByChunks(src, middleBufferSink, addPadding);
+                digestDataByChunks(src, middleBufferSink);
             } else {
                 src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
                         levelOffset[i + 1], levelOffset[i + 2]));
-                digestDataByChunks(src, middleBufferSink, addPadding);
+                digestDataByChunks(src, middleBufferSink);
             }
 
             // If the output is not full chunk, pad with 0s.
@@ -179,8 +177,7 @@
      * less than the chunk size and padding is desired, feed with extra padding 0 to fill up the
      * chunk before digesting.
      */
-    private void digestDataByChunks(DataSource dataSource, DataSink dataSink, boolean addPadding)
-            throws IOException {
+    private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException {
         long size = dataSource.size();
         long offset = 0;
         for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) {
@@ -198,9 +195,6 @@
             buffer = ByteBuffer.allocate(CHUNK_SIZE);  // initialized to 0.
             dataSource.copyTo(offset, remaining, buffer);
             buffer.rewind();
-            if (!addPadding) {
-                buffer.limit(remaining);
-            }
             byte[] hash = saltedDigest(buffer);
             dataSink.consume(hash, 0, hash.length);
         }