Kaynağa Gözat

Add support to the Android editor for signing and verifying Android apks

- Apk signing and verification is enabled using the apksig library from https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
Fredia Huya-Kouadio 11 ay önce
ebeveyn
işleme
6a9c060883
100 değiştirilmiş dosya ile 28799 ekleme ve 0 silme
  1. 8 0
      .pre-commit-config.yaml
  2. 1 0
      platform/android/java/editor/build.gradle
  3. 1801 0
      platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java
  4. 550 0
      platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java
  5. 173 0
      platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java
  6. 3657 0
      platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java
  7. 65 0
      platform/android/java/editor/src/main/java/com/android/apksig/Constants.java
  8. 2241 0
      platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
  9. 123 0
      platform/android/java/editor/src/main/java/com/android/apksig/Hints.java
  10. 32 0
      platform/android/java/editor/src/main/java/com/android/apksig/README.md
  11. 1325 0
      platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java
  12. 911 0
      platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java
  13. 35 0
      platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java
  14. 32 0
      platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java
  15. 670 0
      platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java
  16. 199 0
      platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
  17. 46 0
      platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java
  18. 40 0
      platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java
  19. 869 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java
  20. 104 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
  21. 104 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
  22. 1444 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
  23. 393 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
  24. 35 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
  25. 61 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
  26. 27 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
  27. 225 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
  28. 53 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java
  29. 30 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
  30. 235 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
  31. 34 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
  32. 364 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
  33. 109 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
  34. 139 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
  35. 286 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
  36. 159 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
  37. 74 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java
  38. 26 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
  39. 586 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
  40. 1570 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
  41. 25 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
  42. 329 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
  43. 471 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
  44. 66 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
  45. 531 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
  46. 783 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
  47. 314 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
  48. 440 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
  49. 267 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
  50. 311 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
  51. 673 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
  52. 28 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java
  53. 32 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java
  54. 596 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
  55. 32 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java
  56. 45 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java
  57. 38 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java
  58. 30 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java
  59. 23 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java
  60. 35 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
  61. 115 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java
  62. 34 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java
  63. 34 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java
  64. 225 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
  65. 208 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java
  66. 313 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java
  67. 363 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java
  68. 127 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java
  69. 61 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java
  70. 463 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java
  71. 173 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
  72. 36 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java
  73. 36 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java
  74. 46 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java
  75. 43 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java
  76. 29 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java
  77. 32 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java
  78. 58 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java
  79. 42 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java
  80. 61 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java
  81. 74 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
  82. 240 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java
  83. 125 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java
  84. 59 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java
  85. 33 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java
  86. 41 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java
  87. 145 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java
  88. 219 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
  89. 191 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java
  90. 68 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java
  91. 89 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java
  92. 51 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java
  93. 77 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java
  94. 81 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java
  95. 104 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java
  96. 51 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java
  97. 325 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
  98. 282 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
  99. 35 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java
  100. 105 0
      platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java

+ 8 - 0
.pre-commit-config.yaml

@@ -17,6 +17,7 @@ repos:
         exclude: |
           (?x)^(
             tests/python_build/.*|
+            platform/android/java/editor/src/main/java/com/android/.*|
             platform/android/java/lib/src/com/.*
           )
 
@@ -30,6 +31,7 @@ repos:
         exclude: |
           (?x)^(
             tests/python_build/.*|
+            platform/android/java/editor/src/main/java/com/android/.*|
             platform/android/java/lib/src/com/.*
           )
         additional_dependencies: [clang-tidy==18.1.1]
@@ -54,6 +56,11 @@ repos:
     rev: v2.3.0
     hooks:
       - id: codespell
+        exclude: |
+          (?x)^(
+            platform/android/java/editor/src/main/java/com/android/.*|
+            platform/android/java/lib/src/com/.*
+          )
         additional_dependencies: [tomli]
 
   ### Requires Docker; look into alternative implementation.
@@ -135,6 +142,7 @@ repos:
           (?x)^(
             core/math/bvh_.*\.inc$|
             platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*|
+            platform/android/java/editor/src/main/java/com/android/.*|
             platform/android/java/lib/src/com/.*|
             platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView\.java$|
             platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper\.java$|

+ 1 - 0
platform/android/java/editor/build.gradle

@@ -12,6 +12,7 @@ dependencies {
     implementation "androidx.window:window:1.3.0"
     implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
     implementation "androidx.constraintlayout:constraintlayout:2.1.4"
+    implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
 }
 
 ext {

+ 1801 - 0
platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java

@@ -0,0 +1,1801 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import static com.android.apksig.Constants.LIBRARY_PAGE_ALIGNMENT_BYTES;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.EocdRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.ReadableDataSink;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK signer.
+ *
+ * <p>The signer preserves as much of the input APK as possible. For example, it preserves the order
+ * of APK entries and preserves their contents, including compressed form and alignment of data.
+ *
+ * <p>Use {@link Builder} to obtain instances of this signer.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class ApkSigner {
+
+    /**
+     * Extensible data block/field header ID used for storing information about alignment of
+     * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
+     * 4.5 Extensible data fields.
+     */
+    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
+
+    /**
+     * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
+     * entries.
+     */
+    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
+
+    private static final short ANDROID_FILE_ALIGNMENT_BYTES = 4096;
+
+    /** Name of the Android manifest ZIP entry in APKs. */
+    private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
+
+    private final List<SignerConfig> mSignerConfigs;
+    private final SignerConfig mSourceStampSignerConfig;
+    private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+    private final boolean mForceSourceStampOverwrite;
+    private final boolean mSourceStampTimestampEnabled;
+    private final Integer mMinSdkVersion;
+    private final int mRotationMinSdkVersion;
+    private final boolean mRotationTargetsDevRelease;
+    private final boolean mV1SigningEnabled;
+    private final boolean mV2SigningEnabled;
+    private final boolean mV3SigningEnabled;
+    private final boolean mV4SigningEnabled;
+    private final boolean mAlignFileSize;
+    private final boolean mVerityEnabled;
+    private final boolean mV4ErrorReportingEnabled;
+    private final boolean mDebuggableApkPermitted;
+    private final boolean mOtherSignersSignaturesPreserved;
+    private final boolean mAlignmentPreserved;
+    private final int mLibraryPageAlignmentBytes;
+    private final String mCreatedBy;
+
+    private final ApkSignerEngine mSignerEngine;
+
+    private final File mInputApkFile;
+    private final DataSource mInputApkDataSource;
+
+    private final File mOutputApkFile;
+    private final DataSink mOutputApkDataSink;
+    private final DataSource mOutputApkDataSource;
+
+    private final File mOutputV4File;
+
+    private final SigningCertificateLineage mSigningCertificateLineage;
+
+    private ApkSigner(
+            List<SignerConfig> signerConfigs,
+            SignerConfig sourceStampSignerConfig,
+            SigningCertificateLineage sourceStampSigningCertificateLineage,
+            boolean forceSourceStampOverwrite,
+            boolean sourceStampTimestampEnabled,
+            Integer minSdkVersion,
+            int rotationMinSdkVersion,
+            boolean rotationTargetsDevRelease,
+            boolean v1SigningEnabled,
+            boolean v2SigningEnabled,
+            boolean v3SigningEnabled,
+            boolean v4SigningEnabled,
+            boolean alignFileSize,
+            boolean verityEnabled,
+            boolean v4ErrorReportingEnabled,
+            boolean debuggableApkPermitted,
+            boolean otherSignersSignaturesPreserved,
+            boolean alignmentPreserved,
+            int libraryPageAlignmentBytes,
+            String createdBy,
+            ApkSignerEngine signerEngine,
+            File inputApkFile,
+            DataSource inputApkDataSource,
+            File outputApkFile,
+            DataSink outputApkDataSink,
+            DataSource outputApkDataSource,
+            File outputV4File,
+            SigningCertificateLineage signingCertificateLineage) {
+
+        mSignerConfigs = signerConfigs;
+        mSourceStampSignerConfig = sourceStampSignerConfig;
+        mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+        mForceSourceStampOverwrite = forceSourceStampOverwrite;
+        mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
+        mMinSdkVersion = minSdkVersion;
+        mRotationMinSdkVersion = rotationMinSdkVersion;
+        mRotationTargetsDevRelease = rotationTargetsDevRelease;
+        mV1SigningEnabled = v1SigningEnabled;
+        mV2SigningEnabled = v2SigningEnabled;
+        mV3SigningEnabled = v3SigningEnabled;
+        mV4SigningEnabled = v4SigningEnabled;
+        mAlignFileSize = alignFileSize;
+        mVerityEnabled = verityEnabled;
+        mV4ErrorReportingEnabled = v4ErrorReportingEnabled;
+        mDebuggableApkPermitted = debuggableApkPermitted;
+        mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+        mAlignmentPreserved = alignmentPreserved;
+        mLibraryPageAlignmentBytes = libraryPageAlignmentBytes;
+        mCreatedBy = createdBy;
+
+        mSignerEngine = signerEngine;
+
+        mInputApkFile = inputApkFile;
+        mInputApkDataSource = inputApkDataSource;
+
+        mOutputApkFile = outputApkFile;
+        mOutputApkDataSink = outputApkDataSink;
+        mOutputApkDataSource = outputApkDataSource;
+
+        mOutputV4File = outputV4File;
+
+        mSigningCertificateLineage = signingCertificateLineage;
+    }
+
+    /**
+     * Signs the input APK and outputs the resulting signed APK. The input APK is not modified.
+     *
+     * @throws IOException if an I/O error is encountered while reading or writing the APKs
+     * @throws ApkFormatException if the input APK is malformed
+     * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
+     *     a required cryptographic algorithm implementation is missing
+     * @throws InvalidKeyException if a signature could not be generated because a signing key is
+     *     not suitable for generating the signature
+     * @throws SignatureException if an error occurred while generating or verifying a signature
+     * @throws IllegalStateException if this signer's configuration is missing required information
+     *     or if the signing engine is in an invalid state.
+     */
+    public void sign()
+            throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+                    SignatureException, IllegalStateException {
+        Closeable in = null;
+        DataSource inputApk;
+        try {
+            if (mInputApkDataSource != null) {
+                inputApk = mInputApkDataSource;
+            } else if (mInputApkFile != null) {
+                RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r");
+                in = inputFile;
+                inputApk = DataSources.asDataSource(inputFile);
+            } else {
+                throw new IllegalStateException("Input APK not specified");
+            }
+
+            Closeable out = null;
+            try {
+                DataSink outputApkOut;
+                DataSource outputApkIn;
+                if (mOutputApkDataSink != null) {
+                    outputApkOut = mOutputApkDataSink;
+                    outputApkIn = mOutputApkDataSource;
+                } else if (mOutputApkFile != null) {
+                    RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw");
+                    out = outputFile;
+                    outputFile.setLength(0);
+                    outputApkOut = DataSinks.asDataSink(outputFile);
+                    outputApkIn = DataSources.asDataSource(outputFile);
+                } else {
+                    throw new IllegalStateException("Output APK not specified");
+                }
+
+                sign(inputApk, outputApkOut, outputApkIn);
+            } finally {
+                if (out != null) {
+                    out.close();
+                }
+            }
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+    }
+
+    private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+                    SignatureException {
+        // Step 1. Find input APK's main ZIP sections
+        ApkUtils.ZipSections inputZipSections;
+        try {
+            inputZipSections = ApkUtils.findZipSections(inputApk);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+        }
+        long inputApkSigningBlockOffset = -1;
+        DataSource inputApkSigningBlock = null;
+        try {
+            ApkUtils.ApkSigningBlock apkSigningBlockInfo =
+                    ApkUtils.findApkSigningBlock(inputApk, inputZipSections);
+            inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+            inputApkSigningBlock = apkSigningBlockInfo.getContents();
+        } catch (ApkSigningBlockNotFoundException e) {
+            // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to
+            // contain this block. It's only needed if the APK is signed using APK Signature Scheme
+            // v2 and/or v3.
+        }
+        DataSource inputApkLfhSection =
+                inputApk.slice(
+                        0,
+                        (inputApkSigningBlockOffset != -1)
+                                ? inputApkSigningBlockOffset
+                                : inputZipSections.getZipCentralDirectoryOffset());
+
+        // Step 2. Parse the input APK's ZIP Central Directory
+        ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
+        List<CentralDirectoryRecord> inputCdRecords =
+                parseZipCentralDirectory(inputCd, inputZipSections);
+
+        List<Hints.PatternWithRange> pinPatterns =
+                extractPinPatterns(inputCdRecords, inputApkLfhSection);
+        List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
+
+        // Step 3. Obtain a signer engine instance
+        ApkSignerEngine signerEngine;
+        if (mSignerEngine != null) {
+            // Use the provided signer engine
+            signerEngine = mSignerEngine;
+        } else {
+            // Construct a signer engine from the provided parameters
+            int minSdkVersion;
+            if (mMinSdkVersion != null) {
+                // No need to extract minSdkVersion from the APK's AndroidManifest.xml
+                minSdkVersion = mMinSdkVersion;
+            } else {
+                // Need to extract minSdkVersion from the APK's AndroidManifest.xml
+                minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection);
+            }
+            List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
+                    new ArrayList<>(mSignerConfigs.size());
+            for (SignerConfig signerConfig : mSignerConfigs) {
+                DefaultApkSignerEngine.SignerConfig.Builder signerConfigBuilder =
+                        new DefaultApkSignerEngine.SignerConfig.Builder(
+                                signerConfig.getName(),
+                                signerConfig.getPrivateKey(),
+                                signerConfig.getCertificates(),
+                                signerConfig.getDeterministicDsaSigning());
+                int signerMinSdkVersion = signerConfig.getMinSdkVersion();
+                SigningCertificateLineage signerLineage =
+                        signerConfig.getSigningCertificateLineage();
+                if (signerMinSdkVersion > 0) {
+                    signerConfigBuilder.setLineageForMinSdkVersion(signerLineage,
+                            signerMinSdkVersion);
+                }
+                engineSignerConfigs.add(signerConfigBuilder.build());
+            }
+            DefaultApkSignerEngine.Builder signerEngineBuilder =
+                    new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
+                            .setV1SigningEnabled(mV1SigningEnabled)
+                            .setV2SigningEnabled(mV2SigningEnabled)
+                            .setV3SigningEnabled(mV3SigningEnabled)
+                            .setVerityEnabled(mVerityEnabled)
+                            .setDebuggableApkPermitted(mDebuggableApkPermitted)
+                            .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
+                            .setSigningCertificateLineage(mSigningCertificateLineage)
+                            .setMinSdkVersionForRotation(mRotationMinSdkVersion)
+                            .setRotationTargetsDevRelease(mRotationTargetsDevRelease);
+            if (mCreatedBy != null) {
+                signerEngineBuilder.setCreatedBy(mCreatedBy);
+            }
+            if (mSourceStampSignerConfig != null) {
+                signerEngineBuilder.setStampSignerConfig(
+                        new DefaultApkSignerEngine.SignerConfig.Builder(
+                                        mSourceStampSignerConfig.getName(),
+                                        mSourceStampSignerConfig.getPrivateKey(),
+                                        mSourceStampSignerConfig.getCertificates(),
+                                        mSourceStampSignerConfig.getDeterministicDsaSigning())
+                                .build());
+                signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled);
+            }
+            if (mSourceStampSigningCertificateLineage != null) {
+                signerEngineBuilder.setSourceStampSigningCertificateLineage(
+                        mSourceStampSigningCertificateLineage);
+            }
+            signerEngine = signerEngineBuilder.build();
+        }
+
+        // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
+        if (inputApkSigningBlock != null) {
+            signerEngine.inputApkSigningBlock(inputApkSigningBlock);
+        }
+
+        // Step 5. Iterate over input APK's entries and output the Local File Header + data of those
+        // entries which need to be output. Entries are iterated in the order in which their Local
+        // File Header records are stored in the file. This is to achieve better data locality in
+        // case Central Directory entries are in the wrong order.
+        List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
+                new ArrayList<>(inputCdRecords);
+        Collections.sort(
+                inputCdRecordsSortedByLfhOffset,
+                CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+        int lastModifiedDateForNewEntries = -1;
+        int lastModifiedTimeForNewEntries = -1;
+        long inputOffset = 0;
+        long outputOffset = 0;
+        byte[] sourceStampCertificateDigest = null;
+        Map<String, CentralDirectoryRecord> outputCdRecordsByName =
+                new HashMap<>(inputCdRecords.size());
+        for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
+            String entryName = inputCdRecord.getName();
+            if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
+                continue; // We'll re-add below if needed.
+            }
+            if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) {
+                try {
+                    sourceStampCertificateDigest =
+                            LocalFileRecord.getUncompressedData(
+                                    inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+                } catch (ZipFormatException ex) {
+                    throw new ApkFormatException("Bad source stamp entry");
+                }
+                continue; // Existing source stamp is handled below as needed.
+            }
+            ApkSignerEngine.InputJarEntryInstructions entryInstructions =
+                    signerEngine.inputJarEntry(entryName);
+            boolean shouldOutput;
+            switch (entryInstructions.getOutputPolicy()) {
+                case OUTPUT:
+                    shouldOutput = true;
+                    break;
+                case OUTPUT_BY_ENGINE:
+                case SKIP:
+                    shouldOutput = false;
+                    break;
+                default:
+                    throw new RuntimeException(
+                            "Unknown output policy: " + entryInstructions.getOutputPolicy());
+            }
+
+            long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset();
+            if (inputLocalFileHeaderStartOffset > inputOffset) {
+                // Unprocessed data in input starting at inputOffset and ending and the start of
+                // this record's LFH. We output this data verbatim because this signer is supposed
+                // to preserve as much of input as possible.
+                long chunkSize = inputLocalFileHeaderStartOffset - inputOffset;
+                inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+                outputOffset += chunkSize;
+                inputOffset = inputLocalFileHeaderStartOffset;
+            }
+            LocalFileRecord inputLocalFileRecord;
+            try {
+                inputLocalFileRecord =
+                        LocalFileRecord.getRecord(
+                                inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e);
+            }
+            inputOffset += inputLocalFileRecord.getSize();
+
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                    entryInstructions.getInspectJarEntryRequest();
+            if (inspectEntryRequest != null) {
+                fulfillInspectInputJarEntryRequest(
+                        inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+            }
+
+            if (shouldOutput) {
+                // Find the max value of last modified, to be used for new entries added by the
+                // signer.
+                int lastModifiedDate = inputCdRecord.getLastModificationDate();
+                int lastModifiedTime = inputCdRecord.getLastModificationTime();
+                if ((lastModifiedDateForNewEntries == -1)
+                        || (lastModifiedDate > lastModifiedDateForNewEntries)
+                        || ((lastModifiedDate == lastModifiedDateForNewEntries)
+                                && (lastModifiedTime > lastModifiedTimeForNewEntries))) {
+                    lastModifiedDateForNewEntries = lastModifiedDate;
+                    lastModifiedTimeForNewEntries = lastModifiedTime;
+                }
+
+                inspectEntryRequest = signerEngine.outputJarEntry(entryName);
+                if (inspectEntryRequest != null) {
+                    fulfillInspectInputJarEntryRequest(
+                            inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+                }
+
+                // Output entry's Local File Header + data
+                long outputLocalFileHeaderOffset = outputOffset;
+                OutputSizeAndDataOffset outputLfrResult =
+                        outputInputJarEntryLfhRecord(
+                                inputApkLfhSection,
+                                inputLocalFileRecord,
+                                outputApkOut,
+                                outputLocalFileHeaderOffset);
+                outputOffset += outputLfrResult.outputBytes;
+                long outputDataOffset =
+                        outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes;
+
+                if (pinPatterns != null) {
+                    boolean pinFileHeader = false;
+                    for (Hints.PatternWithRange pinPattern : pinPatterns) {
+                        if (pinPattern.matcher(inputCdRecord.getName()).matches()) {
+                            Hints.ByteRange dataRange =
+                                    new Hints.ByteRange(outputDataOffset, outputOffset);
+                            Hints.ByteRange pinRange =
+                                    pinPattern.ClampToAbsoluteByteRange(dataRange);
+                            if (pinRange != null) {
+                                pinFileHeader = true;
+                                pinByteRanges.add(pinRange);
+                            }
+                        }
+                    }
+                    if (pinFileHeader) {
+                        pinByteRanges.add(
+                                new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset));
+                    }
+                }
+
+                // Enqueue entry's Central Directory record for output
+                CentralDirectoryRecord outputCdRecord;
+                if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) {
+                    outputCdRecord = inputCdRecord;
+                } else {
+                    outputCdRecord =
+                            inputCdRecord.createWithModifiedLocalFileHeaderOffset(
+                                    outputLocalFileHeaderOffset);
+                }
+                outputCdRecordsByName.put(entryName, outputCdRecord);
+            }
+        }
+        long inputLfhSectionSize = inputApkLfhSection.size();
+        if (inputOffset < inputLfhSectionSize) {
+            // Unprocessed data in input starting at inputOffset and ending and the end of the input
+            // APK's LFH section. We output this data verbatim because this signer is supposed
+            // to preserve as much of input as possible.
+            long chunkSize = inputLfhSectionSize - inputOffset;
+            inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+            outputOffset += chunkSize;
+            inputOffset = inputLfhSectionSize;
+        }
+
+        // Step 6. Sort output APK's Central Directory records in the order in which they should
+        // appear in the output
+        List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
+        for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
+            String entryName = inputCdRecord.getName();
+            CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
+            if (outputCdRecord != null) {
+                outputCdRecords.add(outputCdRecord);
+            }
+        }
+
+        if (lastModifiedDateForNewEntries == -1) {
+            lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+            lastModifiedTimeForNewEntries = 0;
+        }
+
+        // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output
+        // more Local File Header + data entries and add to the list of output Central Directory
+        // records.
+        if (signerEngine.isEligibleForSourceStamp()) {
+            byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
+            if (mForceSourceStampOverwrite
+                    || sourceStampCertificateDigest == null
+                    || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) {
+                outputOffset +=
+                        outputDataToOutputApk(
+                                SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
+                                uncompressedData,
+                                outputOffset,
+                                outputCdRecords,
+                                lastModifiedTimeForNewEntries,
+                                lastModifiedDateForNewEntries,
+                                outputApkOut);
+            } else {
+                throw new ApkFormatException(
+                        String.format(
+                                "Cannot generate SourceStamp. APK contains an existing entry with"
+                                    + " the name: %s, and it is different than the provided source"
+                                    + " stamp certificate",
+                                SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME));
+            }
+        }
+
+        // Step 7.5. Generate pinlist.meta file if necessary.
+        // This has to be before the step 8 so that the file is signed.
+        if (pinByteRanges != null) {
+            // Covers JAR signature and zip central dir entry.
+            // The signature files don't have to be pinned, but pinning them isn't that wasteful
+            // since the total size is small.
+            pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE));
+            String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
+            byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
+
+            requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
+            outputOffset +=
+                outputDataToOutputApk(
+                    entryName,
+                    uncompressedData,
+                    outputOffset,
+                    outputCdRecords,
+                    lastModifiedTimeForNewEntries,
+                    lastModifiedDateForNewEntries,
+                    outputApkOut);
+        }
+
+        // Step 8. Generate and output JAR signatures, if necessary. This may output more Local File
+        // Header + data entries and add to the list of output Central Directory records.
+        ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
+                signerEngine.outputJarEntries();
+        if (outputJarSignatureRequest != null) {
+            for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
+                    outputJarSignatureRequest.getAdditionalJarEntries()) {
+                String entryName = entry.getName();
+                byte[] uncompressedData = entry.getData();
+
+                requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
+                outputOffset +=
+                        outputDataToOutputApk(
+                                entryName,
+                                uncompressedData,
+                                outputOffset,
+                                outputCdRecords,
+                                lastModifiedTimeForNewEntries,
+                                lastModifiedDateForNewEntries,
+                                outputApkOut);
+            }
+            outputJarSignatureRequest.done();
+        }
+
+        // Step 9. Construct output ZIP Central Directory in an in-memory buffer
+        long outputCentralDirSizeBytes = 0;
+        for (CentralDirectoryRecord record : outputCdRecords) {
+            outputCentralDirSizeBytes += record.getSize();
+        }
+        if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
+            throw new IOException(
+                    "Output ZIP Central Directory too large: "
+                            + outputCentralDirSizeBytes
+                            + " bytes");
+        }
+        ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
+        for (CentralDirectoryRecord record : outputCdRecords) {
+            record.copyTo(outputCentralDir);
+        }
+        outputCentralDir.flip();
+        DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
+        long outputCentralDirStartOffset = outputOffset;
+        int outputCentralDirRecordCount = outputCdRecords.size();
+
+        // Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer
+        // because it can be adjusted in Step 11 due to signing block.
+        //   - CD offset (it's shifted by signing block)
+        //   - Comments (when the output file needs to be sized 4k-aligned)
+        ByteBuffer outputEocd =
+                EocdRecord.createWithModifiedCentralDirectoryInfo(
+                        inputZipSections.getZipEndOfCentralDirectory(),
+                        outputCentralDirRecordCount,
+                        outputCentralDirDataSource.size(),
+                        outputCentralDirStartOffset);
+
+        // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or
+        // SourceStamp signatures, if necessary.
+        // This may insert an APK Signing Block just before the output's ZIP Central Directory
+        ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest =
+                signerEngine.outputZipSections2(
+                        outputApkIn,
+                        outputCentralDirDataSource,
+                        DataSources.asDataSource(outputEocd));
+
+        if (outputApkSigningBlockRequest != null) {
+            int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock();
+            byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+            outputApkSigningBlockRequest.done();
+
+            long fileSize =
+                    outputCentralDirStartOffset
+                            + outputCentralDirDataSource.size()
+                            + padding
+                            + outputApkSigningBlock.length
+                            + outputEocd.remaining();
+            if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) {
+                int eocdPadding =
+                        (int)
+                                (ANDROID_FILE_ALIGNMENT_BYTES
+                                        - fileSize % ANDROID_FILE_ALIGNMENT_BYTES);
+                // Replace EOCD with padding one so that output file size can be the multiples of
+                // alignment.
+                outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding);
+
+                // Since EoCD has changed, we need to regenerate signing block as well.
+                outputApkSigningBlockRequest =
+                        signerEngine.outputZipSections2(
+                                outputApkIn,
+                                new ByteBufferDataSource(outputCentralDir),
+                                DataSources.asDataSource(outputEocd));
+                outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+                outputApkSigningBlockRequest.done();
+            }
+
+            outputApkOut.consume(ByteBuffer.allocate(padding));
+            outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
+            ZipUtils.setZipEocdCentralDirectoryOffset(
+                    outputEocd,
+                    outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
+        }
+
+        // Step 12. Output ZIP Central Directory and ZIP End of Central Directory
+        outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
+        outputApkOut.consume(outputEocd);
+        signerEngine.outputDone();
+
+        // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
+        if (mV4SigningEnabled) {
+            signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled);
+        }
+    }
+
+    private static void requestOutputEntryInspection(
+            ApkSignerEngine signerEngine,
+            String entryName,
+            byte[] uncompressedData)
+            throws IOException {
+        ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                signerEngine.outputJarEntry(entryName);
+        if (inspectEntryRequest != null) {
+            inspectEntryRequest.getDataSink().consume(
+                    uncompressedData, 0, uncompressedData.length);
+            inspectEntryRequest.done();
+        }
+    }
+
+    private static long outputDataToOutputApk(
+            String entryName,
+            byte[] uncompressedData,
+            long localFileHeaderOffset,
+            List<CentralDirectoryRecord> outputCdRecords,
+            int lastModifiedTimeForNewEntries,
+            int lastModifiedDateForNewEntries,
+            DataSink outputApkOut)
+            throws IOException {
+        ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+        byte[] compressedData = deflateResult.output;
+        long uncompressedDataCrc32 = deflateResult.inputCrc32;
+        long numOfDataBytes =
+                LocalFileRecord.outputRecordWithDeflateCompressedData(
+                        entryName,
+                        lastModifiedTimeForNewEntries,
+                        lastModifiedDateForNewEntries,
+                        compressedData,
+                        uncompressedDataCrc32,
+                        uncompressedData.length,
+                        outputApkOut);
+        outputCdRecords.add(
+                CentralDirectoryRecord.createWithDeflateCompressedData(
+                        entryName,
+                        lastModifiedTimeForNewEntries,
+                        lastModifiedDateForNewEntries,
+                        uncompressedDataCrc32,
+                        compressedData.length,
+                        uncompressedData.length,
+                        localFileHeaderOffset));
+        return numOfDataBytes;
+    }
+
+    private static void fulfillInspectInputJarEntryRequest(
+            DataSource lfhSection,
+            LocalFileRecord localFileRecord,
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
+            throws IOException, ApkFormatException {
+        try {
+            localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Malformed ZIP entry: " + localFileRecord.getName(), e);
+        }
+        inspectEntryRequest.done();
+    }
+
+    private static class OutputSizeAndDataOffset {
+        public long outputBytes;
+        public long dataOffsetBytes;
+
+        public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) {
+            this.outputBytes = outputBytes;
+            this.dataOffsetBytes = dataOffsetBytes;
+        }
+    }
+
+    private OutputSizeAndDataOffset outputInputJarEntryLfhRecord(
+            DataSource inputLfhSection,
+            LocalFileRecord inputRecord,
+            DataSink outputLfhSection,
+            long outputOffset)
+            throws IOException {
+        long inputOffset = inputRecord.getStartOffsetInArchive();
+        if (inputOffset == outputOffset && mAlignmentPreserved) {
+            // This record's data will be aligned same as in the input APK.
+            return new OutputSizeAndDataOffset(
+                    inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+                    inputRecord.getDataStartOffsetInRecord());
+        }
+        int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
+        if ((dataAlignmentMultiple <= 1)
+                || ((inputOffset % dataAlignmentMultiple) == (outputOffset % dataAlignmentMultiple)
+                        && mAlignmentPreserved)) {
+            // This record's data will be aligned same as in the input APK.
+            return new OutputSizeAndDataOffset(
+                    inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+                    inputRecord.getDataStartOffsetInRecord());
+        }
+
+        long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
+        if ((inputDataStartOffset % dataAlignmentMultiple) != 0 && mAlignmentPreserved) {
+            // This record's data is not aligned in the input APK. No need to align it in the
+            // output.
+            return new OutputSizeAndDataOffset(
+                    inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+                    inputRecord.getDataStartOffsetInRecord());
+        }
+
+        // This record's data needs to be re-aligned in the output. This is achieved using the
+        // record's extra field.
+        ByteBuffer aligningExtra =
+                createExtraFieldToAlignData(
+                        inputRecord.getExtra(),
+                        outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
+                        dataAlignmentMultiple);
+        long dataOffset =
+                (long) inputRecord.getDataStartOffsetInRecord()
+                        + aligningExtra.remaining()
+                        - inputRecord.getExtra().remaining();
+        return new OutputSizeAndDataOffset(
+                inputRecord.outputRecordWithModifiedExtra(
+                        inputLfhSection, aligningExtra, outputLfhSection),
+                dataOffset);
+    }
+
+    private int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
+        if (entry.isDataCompressed()) {
+            // Compressed entries don't need to be aligned
+            return 1;
+        }
+
+        // Attempt to obtain the alignment multiple from the entry's extra field.
+        ByteBuffer extra = entry.getExtra();
+        if (extra.hasRemaining()) {
+            extra.order(ByteOrder.LITTLE_ENDIAN);
+            // FORMAT: sequence of fields. Each field consists of:
+            //   * uint16 ID
+            //   * uint16 size
+            //   * 'size' bytes: payload
+            while (extra.remaining() >= 4) {
+                short headerId = extra.getShort();
+                int dataSize = ZipUtils.getUnsignedInt16(extra);
+                if (dataSize > extra.remaining()) {
+                    // Malformed field -- insufficient input remaining
+                    break;
+                }
+                if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
+                    // Skip this field
+                    extra.position(extra.position() + dataSize);
+                    continue;
+                }
+                // This is APK alignment field.
+                // FORMAT:
+                //  * uint16 alignment multiple (in bytes)
+                //  * remaining bytes -- padding to achieve alignment of data which starts after
+                //    the extra field
+                if (dataSize < 2) {
+                    // Malformed
+                    break;
+                }
+                return ZipUtils.getUnsignedInt16(extra);
+            }
+        }
+
+        // Fall back to filename-based defaults
+        return (entry.getName().endsWith(".so")) ? mLibraryPageAlignmentBytes : 4;
+    }
+
+    private static ByteBuffer createExtraFieldToAlignData(
+            ByteBuffer original, long extraStartOffset, int dataAlignmentMultiple) {
+        if (dataAlignmentMultiple <= 1) {
+            return original;
+        }
+
+        // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1.
+        ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+
+        // Step 1. Output all extra fields other than the one which is to do with alignment
+        // FORMAT: sequence of fields. Each field consists of:
+        //   * uint16 ID
+        //   * uint16 size
+        //   * 'size' bytes: payload
+        while (original.remaining() >= 4) {
+            short headerId = original.getShort();
+            int dataSize = ZipUtils.getUnsignedInt16(original);
+            if (dataSize > original.remaining()) {
+                // Malformed field -- insufficient input remaining
+                break;
+            }
+            if (((headerId == 0) && (dataSize == 0))
+                    || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) {
+                // Ignore the field if it has to do with the old APK data alignment method (filling
+                // the extra field with 0x00 bytes) or the new APK data alignment method.
+                original.position(original.position() + dataSize);
+                continue;
+            }
+            // Copy this field (including header) to the output
+            original.position(original.position() - 4);
+            int originalLimit = original.limit();
+            original.limit(original.position() + 4 + dataSize);
+            result.put(original);
+            original.limit(originalLimit);
+        }
+
+        // Step 2. Add alignment field
+        // FORMAT:
+        //  * uint16 extra header ID
+        //  * uint16 extra data size
+        //        Payload ('data size' bytes)
+        //      * uint16 alignment multiple (in bytes)
+        //      * remaining bytes -- padding to achieve alignment of data which starts after the
+        //        extra field
+        long dataMinStartOffset =
+                extraStartOffset
+                        + result.position()
+                        + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
+        int paddingSizeBytes =
+                (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
+                        % dataAlignmentMultiple;
+        result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
+        ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes);
+        ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple);
+        result.position(result.position() + paddingSizeBytes);
+        result.flip();
+
+        return result;
+    }
+
+    private static ByteBuffer getZipCentralDirectory(
+            DataSource apk, ApkUtils.ZipSections apkSections)
+            throws IOException, ApkFormatException {
+        long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+        if (cdSizeBytes > Integer.MAX_VALUE) {
+            throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+        }
+        long cdOffset = apkSections.getZipCentralDirectoryOffset();
+        ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+        cd.order(ByteOrder.LITTLE_ENDIAN);
+        return cd;
+    }
+
+    private static List<CentralDirectoryRecord> parseZipCentralDirectory(
+            ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException {
+        long cdOffset = apkSections.getZipCentralDirectoryOffset();
+        int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+        List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+        Set<String> entryNames = new HashSet<>(expectedCdRecordCount);
+        for (int i = 0; i < expectedCdRecordCount; i++) {
+            CentralDirectoryRecord cdRecord;
+            int offsetInsideCd = cd.position();
+            try {
+                cdRecord = CentralDirectoryRecord.getRecord(cd);
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException(
+                        "Malformed ZIP Central Directory record #"
+                                + (i + 1)
+                                + " at file offset "
+                                + (cdOffset + offsetInsideCd),
+                        e);
+            }
+            String entryName = cdRecord.getName();
+            if (!entryNames.add(entryName)) {
+                throw new ApkFormatException(
+                        "Multiple ZIP entries with the same name: " + entryName);
+            }
+            cdRecords.add(cdRecord);
+        }
+        if (cd.hasRemaining()) {
+            throw new ApkFormatException(
+                    "Unused space at the end of ZIP Central Directory: "
+                            + cd.remaining()
+                            + " bytes starting at file offset "
+                            + (cdOffset + cd.position()));
+        }
+
+        return cdRecords;
+    }
+
+    private static CentralDirectoryRecord findCdRecord(
+            List<CentralDirectoryRecord> cdRecords, String name) {
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            if (name.equals(cdRecord.getName())) {
+                return cdRecord;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the contents of the APK's {@code AndroidManifest.xml} or {@code null} if this entry
+     * is not present in the APK.
+     */
+    static ByteBuffer getAndroidManifestFromApk(
+            List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
+            throws IOException, ApkFormatException, ZipFormatException {
+        CentralDirectoryRecord androidManifestCdRecord =
+                findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME);
+        if (androidManifestCdRecord == null) {
+            throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
+        }
+
+        return ByteBuffer.wrap(
+                LocalFileRecord.getUncompressedData(
+                        lhfSection, androidManifestCdRecord, lhfSection.size()));
+    }
+
+    /**
+     * Return list of pin patterns embedded in the pin pattern asset file. If no such file, return
+     * {@code null}.
+     */
+    private static List<Hints.PatternWithRange> extractPinPatterns(
+            List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
+            throws IOException, ApkFormatException {
+        CentralDirectoryRecord pinListCdRecord =
+                findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
+        List<Hints.PatternWithRange> pinPatterns = null;
+        if (pinListCdRecord != null) {
+            pinPatterns = new ArrayList<>();
+            byte[] patternBlob;
+            try {
+                patternBlob =
+                        LocalFileRecord.getUncompressedData(
+                                lhfSection, pinListCdRecord, lhfSection.size());
+            } catch (ZipFormatException ex) {
+                throw new ApkFormatException("Bad " + pinListCdRecord);
+            }
+            pinPatterns = Hints.parsePinPatterns(patternBlob);
+        }
+        return pinPatterns;
+    }
+
+    /**
+     * Returns the minimum Android version (API Level) supported by the provided APK. This is based
+     * on the {@code android:minSdkVersion} attributes of the APK's {@code AndroidManifest.xml}.
+     */
+    private static int getMinSdkVersionFromApk(
+            List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
+            throws IOException, MinSdkVersionException {
+        ByteBuffer androidManifest;
+        try {
+            androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection);
+        } catch (ZipFormatException | ApkFormatException e) {
+            throw new MinSdkVersionException(
+                    "Failed to determine APK's minimum supported Android platform version", e);
+        }
+        return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
+    }
+
+    /**
+     * Configuration of a signer.
+     *
+     * <p>Use {@link Builder} to obtain configuration instances.
+     */
+    public static class SignerConfig {
+        private final String mName;
+        private final PrivateKey mPrivateKey;
+        private final List<X509Certificate> mCertificates;
+        private final boolean mDeterministicDsaSigning;
+        private final int mMinSdkVersion;
+        private final SigningCertificateLineage mSigningCertificateLineage;
+
+        private SignerConfig(Builder builder) {
+            mName = builder.mName;
+            mPrivateKey = builder.mPrivateKey;
+            mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
+            mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
+            mMinSdkVersion = builder.mMinSdkVersion;
+            mSigningCertificateLineage = builder.mSigningCertificateLineage;
+        }
+
+        /** Returns the name of this signer. */
+        public String getName() {
+            return mName;
+        }
+
+        /** Returns the signing key of this signer. */
+        public PrivateKey getPrivateKey() {
+            return mPrivateKey;
+        }
+
+        /**
+         * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+         * to this signer's private key.
+         */
+        public List<X509Certificate> getCertificates() {
+            return mCertificates;
+        }
+
+        /**
+         * If this signer is a DSA signer, whether or not the signing is done deterministically.
+         */
+        public boolean getDeterministicDsaSigning() {
+            return mDeterministicDsaSigning;
+        }
+
+        /** Returns the minimum SDK version for which this signer should be used. */
+        public int getMinSdkVersion() {
+            return mMinSdkVersion;
+        }
+
+        /** Returns the {@link SigningCertificateLineage} for this signer. */
+        public SigningCertificateLineage getSigningCertificateLineage() {
+            return mSigningCertificateLineage;
+        }
+
+        /** Builder of {@link SignerConfig} instances. */
+        public static class Builder {
+            private final String mName;
+            private final PrivateKey mPrivateKey;
+            private final List<X509Certificate> mCertificates;
+            private final boolean mDeterministicDsaSigning;
+
+            private int mMinSdkVersion;
+            private SigningCertificateLineage mSigningCertificateLineage;
+
+            /**
+             * Constructs a new {@code Builder}.
+             *
+             * @param name signer's name. The name is reflected in the name of files comprising the
+             *     JAR signature of the APK.
+             * @param privateKey signing key
+             * @param certificates list of one or more X.509 certificates. The subject public key of
+             *     the first certificate must correspond to the {@code privateKey}.
+             */
+            public Builder(
+                    String name,
+                    PrivateKey privateKey,
+                    List<X509Certificate> certificates) {
+                this(name, privateKey, certificates, false);
+            }
+
+            /**
+             * Constructs a new {@code Builder}.
+             *
+             * @param name signer's name. The name is reflected in the name of files comprising the
+             *     JAR signature of the APK.
+             * @param privateKey signing key
+             * @param certificates list of one or more X.509 certificates. The subject public key of
+             *     the first certificate must correspond to the {@code privateKey}.
+             * @param deterministicDsaSigning When signing using DSA, whether or not the
+             *     deterministic variant (RFC6979) should be used.
+             */
+            public Builder(
+                    String name,
+                    PrivateKey privateKey,
+                    List<X509Certificate> certificates,
+                    boolean deterministicDsaSigning) {
+                if (name.isEmpty()) {
+                    throw new IllegalArgumentException("Empty name");
+                }
+                mName = name;
+                mPrivateKey = privateKey;
+                mCertificates = new ArrayList<>(certificates);
+                mDeterministicDsaSigning = deterministicDsaSigning;
+            }
+
+            /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
+            public Builder setMinSdkVersion(int minSdkVersion) {
+                return setLineageForMinSdkVersion(null, minSdkVersion);
+            }
+
+            /**
+             * Sets the specified {@code minSdkVersion} as the minimum Android platform version
+             * (API level) for which the provided {@code lineage} (where applicable) should be used
+             * to produce the APK's signature. This method is useful if callers want to specify a
+             * particular rotated signer or lineage with restricted capabilities for later
+             * platform releases.
+             *
+             * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
+             * signing lineages with capabilities; only an app's original signer(s) can be used for
+             * the V1 and V2 signature blocks. Because of this, only a value of {@code
+             * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
+             * introduced can be specified.
+             *
+             * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
+             * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
+             * the current {@code SignerConfig} being used in the V3.0 signing block and applied to
+             * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
+             * subsequent {@code SignerConfig} instances). Because of this, only a single {@code
+             * SignerConfig} can be instantiated with a minimum SDK version <= 32.
+             *
+             * @param lineage the {@code SigningCertificateLineage} to target the specified {@code
+             *                minSdkVersion}
+             * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
+             *                      should be used
+             * @return this {@code Builder} instance
+             *
+             * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
+             * certificate provided in the constructor is not in the specified {@code lineage}.
+             */
+            public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
+                    int minSdkVersion) {
+                if (minSdkVersion < AndroidSdkVersion.P) {
+                    throw new IllegalArgumentException(
+                            "SDK targeted signing config is only supported with the V3 signature "
+                                    + "scheme on Android P (SDK version "
+                                    + AndroidSdkVersion.P + ") and later");
+                }
+                if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                    minSdkVersion = AndroidSdkVersion.P;
+                }
+                mMinSdkVersion = minSdkVersion;
+                // If a lineage is provided, ensure the signing certificate for this signer is in
+                // the lineage; in the case of multiple signing certificates, the first is always
+                // used in the lineage.
+                if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
+                    throw new IllegalArgumentException(
+                            "The provided lineage does not contain the signing certificate, "
+                                    + mCertificates.get(0).getSubjectDN()
+                                    + ", for this SignerConfig");
+                }
+                mSigningCertificateLineage = lineage;
+                return this;
+            }
+
+            /**
+             * Returns a new {@code SignerConfig} instance configured based on the configuration of
+             * this builder.
+             */
+            public SignerConfig build() {
+                return new SignerConfig(this);
+            }
+        }
+    }
+
+    /**
+     * Builder of {@link ApkSigner} instances.
+     *
+     * <p>The builder requires the following information to construct a working {@code ApkSigner}:
+     *
+     * <ul>
+     *   <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
+     *   <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
+     *   <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
+     *       variants.
+     * </ul>
+     */
+    public static class Builder {
+        private final List<SignerConfig> mSignerConfigs;
+        private SignerConfig mSourceStampSignerConfig;
+        private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+        private boolean mForceSourceStampOverwrite = false;
+        private boolean mSourceStampTimestampEnabled = true;
+        private boolean mV1SigningEnabled = true;
+        private boolean mV2SigningEnabled = true;
+        private boolean mV3SigningEnabled = true;
+        private boolean mV4SigningEnabled = true;
+        private boolean mAlignFileSize = false;
+        private boolean mVerityEnabled = false;
+        private boolean mV4ErrorReportingEnabled = false;
+        private boolean mDebuggableApkPermitted = true;
+        private boolean mOtherSignersSignaturesPreserved;
+        private boolean mAlignmentPreserved = false;
+        private int mLibraryPageAlignmentBytes = LIBRARY_PAGE_ALIGNMENT_BYTES;
+        private String mCreatedBy;
+        private Integer mMinSdkVersion;
+        private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+        private boolean mRotationTargetsDevRelease = false;
+
+        private final ApkSignerEngine mSignerEngine;
+
+        private File mInputApkFile;
+        private DataSource mInputApkDataSource;
+
+        private File mOutputApkFile;
+        private DataSink mOutputApkDataSink;
+        private DataSource mOutputApkDataSource;
+
+        private File mOutputV4File;
+
+        private SigningCertificateLineage mSigningCertificateLineage;
+
+        // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
+        // signing by default, but not require prior clients to update to explicitly disable v3
+        // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided
+        // inputs (multiple signers and mSigningCertificateLineage in particular).  Maintain two
+        // extra variables to record whether or not mV3SigningEnabled has been set directly by a
+        // client and so should override the default behavior.
+        private boolean mV3SigningExplicitlyDisabled = false;
+        private boolean mV3SigningExplicitlyEnabled = false;
+
+        /**
+         * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
+         * signer configurations. The resulting signer may be further customized through this
+         * builder's setters, such as {@link #setMinSdkVersion(int)}, {@link
+         * #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)}, {@link
+         * #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
+         *
+         * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more
+         * control over low-level details of signing is desired.
+         */
+        public Builder(List<SignerConfig> signerConfigs) {
+            if (signerConfigs.isEmpty()) {
+                throw new IllegalArgumentException("At least one signer config must be provided");
+            }
+            if (signerConfigs.size() > 1) {
+                // APK Signature Scheme v3 only supports single signer, unless a
+                // SigningCertificateLineage is provided, in which case this will be reset to true,
+                // since we don't yet have a v4 scheme about which to worry
+                mV3SigningEnabled = false;
+            }
+            mSignerConfigs = new ArrayList<>(signerConfigs);
+            mSignerEngine = null;
+        }
+
+        /**
+         * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
+         * signing engine. This is meant for advanced use cases where more control is needed over
+         * the lower-level details of signing. For typical use cases, {@link #Builder(List)} is more
+         * appropriate.
+         */
+        public Builder(ApkSignerEngine signerEngine) {
+            if (signerEngine == null) {
+                throw new NullPointerException("signerEngine == null");
+            }
+            mSignerEngine = signerEngine;
+            mSignerConfigs = null;
+        }
+
+        /** Sets the signing configuration of the source stamp to be embedded in the APK. */
+        public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) {
+            mSourceStampSignerConfig = sourceStampSignerConfig;
+            return this;
+        }
+
+        /**
+         * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+         * signing certificate rotation for certificates previously used to sign source stamps.
+         */
+        public Builder setSourceStampSigningCertificateLineage(
+                SigningCertificateLineage sourceStampSigningCertificateLineage) {
+            mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should overwrite existing source stamp, if found.
+         *
+         * @param force {@code true} to require the APK to be overwrite existing source stamp
+         */
+        public Builder setForceSourceStampOverwrite(boolean force) {
+            mForceSourceStampOverwrite = force;
+            return this;
+        }
+
+        /**
+         * Sets whether the source stamp should contain the timestamp attribute with the time
+         * at which the source stamp was signed.
+         */
+        public Builder setSourceStampTimestampEnabled(boolean value) {
+            mSourceStampTimestampEnabled = value;
+            return this;
+        }
+
+        /**
+         * Sets the APK to be signed.
+         *
+         * @see #setInputApk(DataSource)
+         */
+        public Builder setInputApk(File inputApk) {
+            if (inputApk == null) {
+                throw new NullPointerException("inputApk == null");
+            }
+            mInputApkFile = inputApk;
+            mInputApkDataSource = null;
+            return this;
+        }
+
+        /**
+         * Sets the APK to be signed.
+         *
+         * @see #setInputApk(File)
+         */
+        public Builder setInputApk(DataSource inputApk) {
+            if (inputApk == null) {
+                throw new NullPointerException("inputApk == null");
+            }
+            mInputApkDataSource = inputApk;
+            mInputApkFile = null;
+            return this;
+        }
+
+        /**
+         * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if
+         * it doesn't exist.
+         *
+         * @see #setOutputApk(ReadableDataSink)
+         * @see #setOutputApk(DataSink, DataSource)
+         */
+        public Builder setOutputApk(File outputApk) {
+            if (outputApk == null) {
+                throw new NullPointerException("outputApk == null");
+            }
+            mOutputApkFile = outputApk;
+            mOutputApkDataSink = null;
+            mOutputApkDataSource = null;
+            return this;
+        }
+
+        /**
+         * Sets the readable data sink which will receive the output (signed) APK. After signing,
+         * the contents of the output APK will be available via the {@link DataSource} interface of
+         * the sink.
+         *
+         * <p>This variant of {@code setOutputApk} is useful for avoiding writing the output APK to
+         * a file. For example, an in-memory data sink, such as {@link
+         * DataSinks#newInMemoryDataSink()}, could be used instead of a file.
+         *
+         * @see #setOutputApk(File)
+         * @see #setOutputApk(DataSink, DataSource)
+         */
+        public Builder setOutputApk(ReadableDataSink outputApk) {
+            if (outputApk == null) {
+                throw new NullPointerException("outputApk == null");
+            }
+            return setOutputApk(outputApk, outputApk);
+        }
+
+        /**
+         * Sets the sink which will receive the output (signed) APK. Data received by the {@code
+         * outputApkOut} sink must be visible through the {@code outputApkIn} data source.
+         *
+         * <p>This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the
+         * sink and the source to be different objects.
+         *
+         * @see #setOutputApk(ReadableDataSink)
+         * @see #setOutputApk(File)
+         */
+        public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) {
+            if (outputApkOut == null) {
+                throw new NullPointerException("outputApkOut == null");
+            }
+            if (outputApkIn == null) {
+                throw new NullPointerException("outputApkIn == null");
+            }
+            mOutputApkFile = null;
+            mOutputApkDataSink = outputApkOut;
+            mOutputApkDataSource = outputApkIn;
+            return this;
+        }
+
+        /**
+         * Sets the location of the V4 output file. {@code ApkSigner} will create this file if it
+         * doesn't exist.
+         */
+        public Builder setV4SignatureOutputFile(File v4SignatureOutputFile) {
+            if (v4SignatureOutputFile == null) {
+                throw new NullPointerException("v4HashRootOutputFile == null");
+            }
+            mOutputV4File = v4SignatureOutputFile;
+            return this;
+        }
+
+        /**
+         * Sets the minimum Android platform version (API Level) on which APK signatures produced by
+         * the signer being built must verify. This method is useful for overriding the default
+         * behavior where the minimum API Level is obtained from the {@code android:minSdkVersion}
+         * attribute of the APK's {@code AndroidManifest.xml}.
+         *
+         * <p><em>Note:</em> This method may result in APK signatures which don't verify on some
+         * Android platform versions supported by the APK.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         */
+        public Builder setMinSdkVersion(int minSdkVersion) {
+            checkInitializedWithoutEngine();
+            mMinSdkVersion = minSdkVersion;
+            return this;
+        }
+
+        /**
+         * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+         * key should be used to produce the APK's signature. The original signing key for the APK
+         * will be used for all previous platform versions. If a rotated key with signing lineage is
+         * not provided then this method is a noop. This method is useful for overriding the
+         * default behavior where Android T is set as the minimum API level for rotation.
+         *
+         * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+         * in the original V3 signing block being used without platform targeting.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         */
+        public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+            checkInitializedWithoutEngine();
+            // If the provided SDK version does not support v3.1, then use the default SDK version
+            // with rotation support.
+            if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+            } else {
+                mRotationMinSdkVersion = minSdkVersion;
+            }
+            return this;
+        }
+
+        /**
+         * Sets whether the rotation-min-sdk-version is intended to target a development release;
+         * this is primarily required after the T SDK is finalized, and an APK needs to target U
+         * during its development cycle for rotation.
+         *
+         * <p>This is only required after the T SDK is finalized since S and earlier releases do
+         * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+         * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+         * SDK version along with setting {@code enabled} to true will allow an APK to use the
+         * rotated key on a device running U while causing this to be bypassed for T.
+         *
+         * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+         * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+         * will be a noop.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         */
+        public Builder setRotationTargetsDevRelease(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mRotationTargetsDevRelease = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+         *
+         * <p>By default, whether APK is signed using JAR signing is determined by {@code
+         * ApkSigner}, based on the platform versions supported by the APK or specified using {@link
+         * #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which don't
+         * verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @param enabled {@code true} to require the APK to be signed using JAR signing, {@code
+         *     false} to require the APK to not be signed using JAR signing.
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         * @see <a
+         *     href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR
+         *     signing</a>
+         */
+        public Builder setV1SigningEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV1SigningEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
+         * scheme).
+         *
+         * <p>By default, whether APK is signed using APK Signature Scheme v2 is determined by
+         * {@code ApkSigner} based on the platform versions supported by the APK or specified using
+         * {@link #setMinSdkVersion(int)}.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
+         *     v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2.
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature
+         *     Scheme v2</a>
+         */
+        public Builder setV2SigningEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV2SigningEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature
+         * scheme).
+         *
+         * <p>By default, whether APK is signed using APK Signature Scheme v3 is determined by
+         * {@code ApkSigner} based on the platform versions supported by the APK or specified using
+         * {@link #setMinSdkVersion(int)}.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * <p><em>Note:</em> APK Signature Scheme v3 only supports a single signing certificate, but
+         * may take multiple signers mapping to different targeted platform versions.
+         *
+         * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
+         *     v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3.
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         */
+        public Builder setV3SigningEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV3SigningEnabled = enabled;
+            if (enabled) {
+                mV3SigningExplicitlyEnabled = true;
+            } else {
+                mV3SigningExplicitlyDisabled = true;
+            }
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using APK Signature Scheme v4.
+         *
+         * <p>V4 signing requires that the APK be v2 or v3 signed.
+         *
+         * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2
+         *     or v3 and generate an v4 signature file
+         */
+        public Builder setV4SigningEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV4SigningEnabled = enabled;
+            mV4ErrorReportingEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether errors during v4 signing should be reported and halt the signing process.
+         *
+         * <p>Error reporting for v4 signing is disabled by default, but will be enabled if the
+         * caller invokes {@link #setV4SigningEnabled} with a value of true. This method is useful
+         * for tools that enable v4 signing by default but don't want to fail the signing process if
+         * the user did not explicitly request the v4 signing.
+         *
+         * @param enabled {@code false} to prevent errors encountered during the V4 signing from
+         *     halting the signing process
+         */
+        public Builder setV4ErrorReportingEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV4ErrorReportingEnabled = enabled;
+            return this;
+        }
+
+       /**
+         * Sets whether the output APK files should be sized as multiples of 4K.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         */
+        public Builder setAlignFileSize(boolean alignFileSize) {
+            checkInitializedWithoutEngine();
+            mAlignFileSize = alignFileSize;
+            return this;
+        }
+
+        /**
+         * Sets whether to enable the verity signature algorithm for the v2 and v3 signature
+         * schemes.
+         *
+         * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the
+         *     v2 and v3 signature blocks.
+         */
+        public Builder setVerityEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mVerityEnabled = 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}.
+         *
+         * <p>It is dangerous to sign debuggable APKs with production/release keys because Android
+         * platform loosens security checks for such APKs. For example, arbitrary unauthorized code
+         * may be executed in the context of such an app by anybody with ADB shell access.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         */
+        public Builder setDebuggableApkPermitted(boolean permitted) {
+            checkInitializedWithoutEngine();
+            mDebuggableApkPermitted = permitted;
+            return this;
+        }
+
+        /**
+         * Sets whether signatures produced by signers other than the ones configured in this engine
+         * should be copied from the input APK to the output APK.
+         *
+         * <p>By default, signatures of other signers are omitted from the output APK.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         */
+        public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
+            checkInitializedWithoutEngine();
+            mOtherSignersSignaturesPreserved = preserved;
+            return this;
+        }
+
+        /**
+         * Sets the value of the {@code Created-By} field in JAR signature files.
+         *
+         * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+         * with an {@link ApkSignerEngine}.
+         *
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         */
+        public Builder setCreatedBy(String createdBy) {
+            checkInitializedWithoutEngine();
+            if (createdBy == null) {
+                throw new NullPointerException();
+            }
+            mCreatedBy = createdBy;
+            return this;
+        }
+
+        private void checkInitializedWithoutEngine() {
+            if (mSignerEngine != null) {
+                throw new IllegalStateException(
+                        "Operation is not available when builder initialized with an engine");
+            }
+        }
+
+        /**
+         * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
+         * structure provides proof of signing certificate rotation linking {@link SignerConfig}
+         * objects to previous ones.
+         */
+        public Builder setSigningCertificateLineage(
+                SigningCertificateLineage signingCertificateLineage) {
+            if (signingCertificateLineage != null) {
+                mV3SigningEnabled = true;
+                mSigningCertificateLineage = signingCertificateLineage;
+            }
+            return this;
+        }
+
+        /**
+         * Sets whether the existing alignment within the APK should be preserved; the
+         * default for this setting is false. When this value is false, the value provided to
+         * {@link #setLibraryPageAlignmentBytes(int)} will be used to page align native library
+         * files and 4 bytes will be used to align all other uncompressed files.
+         */
+        public Builder setAlignmentPreserved(boolean alignmentPreserved) {
+            mAlignmentPreserved = alignmentPreserved;
+            return this;
+        }
+
+        /**
+         * Sets the number of bytes to be used to page align native library files in the APK; the
+         * default for this setting is {@link Constants#LIBRARY_PAGE_ALIGNMENT_BYTES}.
+         */
+        public Builder setLibraryPageAlignmentBytes(int libraryPageAlignmentBytes) {
+            mLibraryPageAlignmentBytes = libraryPageAlignmentBytes;
+            return this;
+        }
+
+        /**
+         * Returns a new {@code ApkSigner} instance initialized according to the configuration of
+         * this builder.
+         */
+        public ApkSigner build() {
+            if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
+                throw new IllegalStateException(
+                        "Builder configured to both enable and disable APK "
+                                + "Signature Scheme v3 signing");
+            }
+
+            if (mV3SigningExplicitlyDisabled) {
+                mV3SigningEnabled = false;
+            }
+
+            if (mV3SigningExplicitlyEnabled) {
+                mV3SigningEnabled = true;
+            }
+
+            // If V4 signing is not explicitly set, and V2/V3 signing is disabled, then V4 signing
+            // must be disabled as well as it is dependent on V2/V3.
+            if (mV4SigningEnabled && !mV2SigningEnabled && !mV3SigningEnabled) {
+                if (!mV4ErrorReportingEnabled) {
+                    mV4SigningEnabled = false;
+                } else {
+                    throw new IllegalStateException(
+                            "APK Signature Scheme v4 signing requires at least "
+                                    + "v2 or v3 signing to be enabled");
+                }
+            }
+
+            // TODO - if v3 signing is enabled, check provided signers and history to see if valid
+
+            return new ApkSigner(
+                    mSignerConfigs,
+                    mSourceStampSignerConfig,
+                    mSourceStampSigningCertificateLineage,
+                    mForceSourceStampOverwrite,
+                    mSourceStampTimestampEnabled,
+                    mMinSdkVersion,
+                    mRotationMinSdkVersion,
+                    mRotationTargetsDevRelease,
+                    mV1SigningEnabled,
+                    mV2SigningEnabled,
+                    mV3SigningEnabled,
+                    mV4SigningEnabled,
+                    mAlignFileSize,
+                    mVerityEnabled,
+                    mV4ErrorReportingEnabled,
+                    mDebuggableApkPermitted,
+                    mOtherSignersSignaturesPreserved,
+                    mAlignmentPreserved,
+                    mLibraryPageAlignmentBytes,
+                    mCreatedBy,
+                    mSignerEngine,
+                    mInputApkFile,
+                    mInputApkDataSource,
+                    mOutputApkFile,
+                    mOutputApkDataSink,
+                    mOutputApkDataSource,
+                    mOutputV4File,
+                    mSigningCertificateLineage);
+        }
+    }
+}

+ 550 - 0
platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java

@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * APK signing logic which is independent of how input and output APKs are stored, parsed, and
+ * generated.
+ *
+ * <p><h3>Operating Model</h3>
+ *
+ * The abstract operating model is that there is an input APK which is being signed, thus producing
+ * an output APK. In reality, there may be just an output APK being built from scratch, or the input
+ * APK and the output APK may be the same file. Because this engine does not deal with reading and
+ * writing files, it can handle all of these scenarios.
+ *
+ * <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
+ * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
+ * This may be more efficient than signing the APK using a new instance of the engine. See
+ * <a href="#incremental">Incremental Operation</a>.
+ *
+ * <p>In the engine's operating model, a signed APK is produced as follows.
+ * <ol>
+ * <li>JAR entries to be signed are output,</li>
+ * <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
+ *     output,</li>
+ * <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
+ *     to the output.</li>
+ * </ol>
+ *
+ * <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
+ * may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
+ * engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
+ * which tells the client whether the input JAR entry needs to be output. This avoids the need for
+ * the client to hard-code the aspects of APK signing which determine which parts of input must be
+ * ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
+ * client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
+ * APK.
+ *
+ * <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
+ * steps:
+ * <ol>
+ * <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
+ *     for signing multiple APKs.</li>
+ * <li>Locate the input APK's APK Signing Block and provide it to
+ *     {@link #inputApkSigningBlock(DataSource)}.</li>
+ * <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
+ *     whether this entry should be output. The engine may request to inspect the entry.</li>
+ * <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
+ *     inspect the entry.</li>
+ * <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
+ *     that additional JAR entries are output. These entries comprise the output APK's JAR
+ *     signature.</li>
+ * <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
+ *     invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
+ *     an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
+ *     output APK's APK Signature Scheme v2 signature.</li>
+ * <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
+ *     confirm that the output APK is signed.</li>
+ * <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
+ *     engine free any resources it no longer needs.
+ * </ol>
+ *
+ * <p>Some invocations of the engine may provide the client with a task to perform. The client is
+ * expected to perform all requested tasks before proceeding to the next stage of signing. See
+ * documentation of each method about the deadlines for performing the tasks requested by the
+ * method.
+ *
+ * <p><h3 id="incremental">Incremental Operation</h3></a>
+ *
+ * The engine supports incremental operation where a signed APK is produced, then modified and
+ * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
+ * by the developer. Re-signing may be more efficient than signing from scratch.
+ *
+ * <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
+ * {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
+ * {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
+ * and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
+ * these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
+ * APK.
+ *
+ * <p><h3>Output-only Operation</h3>
+ *
+ * The engine's abstract operating model consists of an input APK and an output APK. However, it is
+ * possible to use the engine in output-only mode where the engine's {@code input...} methods are
+ * not invoked. In this mode, the engine has less control over output because it cannot request that
+ * some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
+ * signed and will report an error if cannot do so.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public interface ApkSignerEngine extends Closeable {
+
+    default void setExecutor(RunnablesExecutor executor) {
+        throw new UnsupportedOperationException("setExecutor method is not implemented");
+    }
+
+    /**
+     * Initializes the signer engine with the data already present in the apk (if any). There
+     * might already be data that can be reused if the entries has not been changed.
+     *
+     * @param manifestBytes
+     * @param entryNames
+     * @return set of entry names which were processed by the engine during the initialization, a
+     *         subset of entryNames
+     */
+    default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
+        throw new UnsupportedOperationException("initWith method is not implemented");
+    }
+
+    /**
+     * Indicates to this engine that the input APK contains the provided APK Signing Block. The
+     * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
+     *
+     * @param apkSigningBlock APK signing block of the input APK. The provided data source is
+     *        guaranteed to not be used by the engine after this method terminates.
+     *
+     * @throws IOException if an I/O error occurs while reading the APK Signing Block
+     * @throws ApkFormatException if the APK Signing Block is malformed
+     * @throws IllegalStateException if this engine is closed
+     */
+    void inputApkSigningBlock(DataSource apkSigningBlock)
+            throws IOException, ApkFormatException, IllegalStateException;
+
+    /**
+     * Indicates to this engine that the specified JAR entry was encountered in the input APK.
+     *
+     * <p>When an input entry is updated/changed, it's OK to not invoke
+     * {@link #inputJarEntryRemoved(String)} before invoking this method.
+     *
+     * @return instructions about how to proceed with this entry
+     *
+     * @throws IllegalStateException if this engine is closed
+     */
+    InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
+
+    /**
+     * Indicates to this engine that the specified JAR entry was output.
+     *
+     * <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
+     * requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
+     * data requested by the engine.
+     *
+     * <p>When an already output entry is updated/changed, it's OK to not invoke
+     * {@link #outputJarEntryRemoved(String)} before invoking this method.
+     *
+     * @return request to inspect the entry or {@code null} if the engine does not need to inspect
+     *         the entry. The request must be fulfilled before {@link #outputJarEntries()} is
+     *         invoked.
+     *
+     * @throws IllegalStateException if this engine is closed
+     */
+    InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
+
+    /**
+     * Indicates to this engine that the specified JAR entry was removed from the input. It's safe
+     * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
+     *
+     * @return output policy of this JAR entry. The policy indicates how this input entry affects
+     *         the output APK. The client of this engine should use this information to determine
+     *         how the removal of this input APK's JAR entry affects the output APK.
+     *
+     * @throws IllegalStateException if this engine is closed
+     */
+    InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
+            throws IllegalStateException;
+
+    /**
+     * Indicates to this engine that the specified JAR entry was removed from the output. It's safe
+     * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
+     *
+     * @throws IllegalStateException if this engine is closed
+     */
+    void outputJarEntryRemoved(String entryName) throws IllegalStateException;
+
+    /**
+     * Indicates to this engine that all JAR entries have been output.
+     *
+     * @return request to add JAR signature to the output or {@code null} if there is no need to add
+     *         a JAR signature. The request will contain additional JAR entries to be output. The
+     *         request must be fulfilled before
+     *         {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
+     *
+     * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
+     *         from producing a valid signature. For example, if the engine uses the provided
+     *         {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
+     * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+     *         cryptographic algorithm implementation is missing
+     * @throws InvalidKeyException if a signature could not be generated because a signing key is
+     *         not suitable for generating the signature
+     * @throws SignatureException if an error occurred while generating a signature
+     * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+     *         entries, or if the engine is closed
+     */
+    OutputJarSignatureRequest outputJarEntries()
+            throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+            SignatureException, IllegalStateException;
+
+    /**
+     * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+     *
+     * <p>The provided data sources are guaranteed to not be used by the engine after this method
+     * terminates.
+     *
+     * @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
+     * DataSource)}.
+     *
+     * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+     *        the ZIP entries. In a well-formed archive, this section starts at the start of the
+     *        archive and extends all the way to the ZIP Central Directory.
+     * @param zipCentralDirectory ZIP Central Directory section
+     * @param zipEocd ZIP End of Central Directory (EoCD) record
+     *
+     * @return request to add an APK Signing Block to the output or {@code null} if the output must
+     *         not contain an APK Signing Block. The request must be fulfilled before
+     *         {@link #outputDone()} is invoked.
+     *
+     * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+     * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+     *         engine from producing a valid signature. For example, if the APK Signing Block
+     *         provided to the engine is malformed.
+     * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+     *         cryptographic algorithm implementation is missing
+     * @throws InvalidKeyException if a signature could not be generated because a signing key is
+     *         not suitable for generating the signature
+     * @throws SignatureException if an error occurred while generating a signature
+     * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+     *         entries or to output JAR signature, or if the engine is closed
+     */
+    @Deprecated
+    OutputApkSigningBlockRequest outputZipSections(
+            DataSource zipEntries,
+            DataSource zipCentralDirectory,
+            DataSource zipEocd)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException,
+            InvalidKeyException, SignatureException, IllegalStateException;
+
+    /**
+     * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+     *
+     * <p>The provided data sources are guaranteed to not be used by the engine after this method
+     * terminates.
+     *
+     * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+     *        the ZIP entries. In a well-formed archive, this section starts at the start of the
+     *        archive and extends all the way to the ZIP Central Directory.
+     * @param zipCentralDirectory ZIP Central Directory section
+     * @param zipEocd ZIP End of Central Directory (EoCD) record
+     *
+     * @return request to add an APK Signing Block to the output or {@code null} if the output must
+     *         not contain an APK Signing Block. The request must be fulfilled before
+     *         {@link #outputDone()} is invoked.
+     *
+     * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+     * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+     *         engine from producing a valid signature. For example, if the APK Signing Block
+     *         provided to the engine is malformed.
+     * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+     *         cryptographic algorithm implementation is missing
+     * @throws InvalidKeyException if a signature could not be generated because a signing key is
+     *         not suitable for generating the signature
+     * @throws SignatureException if an error occurred while generating a signature
+     * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+     *         entries or to output JAR signature, or if the engine is closed
+     */
+    OutputApkSigningBlockRequest2 outputZipSections2(
+            DataSource zipEntries,
+            DataSource zipCentralDirectory,
+            DataSource zipEocd)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException,
+            InvalidKeyException, SignatureException, IllegalStateException;
+
+    /**
+     * Indicates to this engine that the signed APK was output.
+     *
+     * <p>This does not change the output APK. The method helps the client confirm that the current
+     * output is signed.
+     *
+     * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+     *         entries or to output signatures, or if the engine is closed
+     */
+    void outputDone() throws IllegalStateException;
+
+    /**
+     * 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 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
+     *         cryptographic algorithm implementation is missing
+     * @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, boolean ignoreFailures)
+            throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
+
+    /**
+     * Checks if the signing configuration provided to the engine is capable of creating a
+     * SourceStamp.
+     */
+    default boolean isEligibleForSourceStamp() {
+        return false;
+    }
+
+    /** Generates the digest of the certificate used to sign the source stamp. */
+    default byte[] generateSourceStampCertificateDigest() throws SignatureException {
+        return new byte[0];
+    }
+
+    /**
+     * Indicates to this engine that it will no longer be used. Invoking this on an already closed
+     * engine is OK.
+     *
+     * <p>This does not change the output APK. For example, if the output APK is not yet fully
+     * signed, it will remain so after this method terminates.
+     */
+    @Override
+    void close();
+
+    /**
+     * Instructions about how to handle an input APK's JAR entry.
+     *
+     * <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
+     * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
+     * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
+     * invoked.
+     */
+    public static class InputJarEntryInstructions {
+        private final OutputPolicy mOutputPolicy;
+        private final InspectJarEntryRequest mInspectJarEntryRequest;
+
+        /**
+         * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+         * output policy and without a request to inspect the entry.
+         */
+        public InputJarEntryInstructions(OutputPolicy outputPolicy) {
+            this(outputPolicy, null);
+        }
+
+        /**
+         * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+         * output mode and with the provided request to inspect the entry.
+         *
+         * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
+         *        need to inspect the entry.
+         */
+        public InputJarEntryInstructions(
+                OutputPolicy outputPolicy,
+                InspectJarEntryRequest inspectJarEntryRequest) {
+            mOutputPolicy = outputPolicy;
+            mInspectJarEntryRequest = inspectJarEntryRequest;
+        }
+
+        /**
+         * Returns the output policy for this entry.
+         */
+        public OutputPolicy getOutputPolicy() {
+            return mOutputPolicy;
+        }
+
+        /**
+         * Returns the request to inspect the JAR entry or {@code null} if there is no need to
+         * inspect the entry.
+         */
+        public InspectJarEntryRequest getInspectJarEntryRequest() {
+            return mInspectJarEntryRequest;
+        }
+
+        /**
+         * Output policy for an input APK's JAR entry.
+         */
+        public static enum OutputPolicy {
+            /** Entry must not be output. */
+            SKIP,
+
+            /** Entry should be output. */
+            OUTPUT,
+
+            /** Entry will be output by the engine. The client can thus ignore this input entry. */
+            OUTPUT_BY_ENGINE,
+        }
+    }
+
+    /**
+     * Request to inspect the specified JAR entry.
+     *
+     * <p>The entry's uncompressed data must be provided to the data sink returned by
+     * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
+     * must be invoked.
+     */
+    interface InspectJarEntryRequest {
+
+        /**
+         * Returns the data sink into which the entry's uncompressed data should be sent.
+         */
+        DataSink getDataSink();
+
+        /**
+         * Indicates that entry's data has been provided in full.
+         */
+        void done();
+
+        /**
+         * Returns the name of the JAR entry.
+         */
+        String getEntryName();
+    }
+
+    /**
+     * Request to add JAR signature (aka v1 signature) to the output APK.
+     *
+     * <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
+     * which {@link #done()} must be invoked.
+     */
+    interface OutputJarSignatureRequest {
+
+        /**
+         * Returns JAR entries that must be added to the output APK.
+         */
+        List<JarEntry> getAdditionalJarEntries();
+
+        /**
+         * Indicates that the JAR entries contained in this request were added to the output APK.
+         */
+        void done();
+
+        /**
+         * JAR entry.
+         */
+        public static class JarEntry {
+            private final String mName;
+            private final byte[] mData;
+
+            /**
+             * Constructs a new {@code JarEntry} with the provided name and data.
+             *
+             * @param data uncompressed data of the entry. Changes to this array will not be
+             *        reflected in {@link #getData()}.
+             */
+            public JarEntry(String name, byte[] data) {
+                mName = name;
+                mData = data.clone();
+            }
+
+            /**
+             * Returns the name of this ZIP entry.
+             */
+            public String getName() {
+                return mName;
+            }
+
+            /**
+             * Returns the uncompressed data of this JAR entry.
+             */
+            public byte[] getData() {
+                return mData.clone();
+            }
+        }
+    }
+
+    /**
+     * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+     * signature(s) of the APK are contained in this block.
+     *
+     * <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+     * output APK such that the block is immediately before the ZIP Central Directory, the offset of
+     * ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
+     * accordingly, and then {@link #done()} must be invoked.
+     *
+     * <p>If the output contains an APK Signing Block, that block must be replaced by the block
+     * contained in this request.
+     *
+     * @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
+     */
+    @Deprecated
+    interface OutputApkSigningBlockRequest {
+
+        /**
+         * Returns the APK Signing Block.
+         */
+        byte[] getApkSigningBlock();
+
+        /**
+         * Indicates that the APK Signing Block was output as requested.
+         */
+        void done();
+    }
+
+    /**
+     * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+     * signature(s) of the APK are contained in this block.
+     *
+     * <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+     * output APK such that the block is immediately before the ZIP Central Directory. Immediately
+     * before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
+     * {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
+     * ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
+     * must be invoked.
+     *
+     * <p>If the output contains an APK Signing Block, that block must be replaced by the block
+     * contained in this request.
+     */
+    interface OutputApkSigningBlockRequest2 {
+        /**
+         * Returns the APK Signing Block.
+         */
+        byte[] getApkSigningBlock();
+
+        /**
+         * Indicates that the APK Signing Block was output as requested.
+         */
+        void done();
+
+        /**
+         * Returns the number of 0x00 bytes the caller must place immediately before APK Signing
+         * Block.
+         */
+        int getPaddingSizeBeforeApkSigningBlock();
+    }
+}

+ 173 - 0
platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java

@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 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;
+
+/**
+ * This class is intended as a lightweight representation of an APK signature verification issue
+ * where the client does not require the additional textual details provided by a subclass.
+ */
+public class ApkVerificationIssue {
+    /* The V2 signer(s) could not be read from the V2 signature block */
+    public static final int V2_SIG_MALFORMED_SIGNERS = 1;
+    /* A V2 signature block exists without any V2 signers */
+    public static final int V2_SIG_NO_SIGNERS = 2;
+    /* Failed to parse a signer's block in the V2 signature block */
+    public static final int V2_SIG_MALFORMED_SIGNER = 3;
+    /* Failed to parse the signer's signature record in the V2 signature block */
+    public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
+    /* The V2 signer contained no signatures */
+    public static final int V2_SIG_NO_SIGNATURES = 5;
+    /* The V2 signer's certificate could not be parsed */
+    public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
+    /* No signing certificates exist for the V2 signer */
+    public static final int V2_SIG_NO_CERTIFICATES = 7;
+    /* Failed to parse the V2 signer's digest record */
+    public static final int V2_SIG_MALFORMED_DIGEST = 8;
+    /* The V3 signer(s) could not be read from the V3 signature block */
+    public static final int V3_SIG_MALFORMED_SIGNERS = 9;
+    /* A V3 signature block exists without any V3 signers */
+    public static final int V3_SIG_NO_SIGNERS = 10;
+    /* Failed to parse a signer's block in the V3 signature block */
+    public static final int V3_SIG_MALFORMED_SIGNER = 11;
+    /* Failed to parse the signer's signature record in the V3 signature block */
+    public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
+    /* The V3 signer contained no signatures */
+    public static final int V3_SIG_NO_SIGNATURES = 13;
+    /* The V3 signer's certificate could not be parsed */
+    public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
+    /* No signing certificates exist for the V3 signer */
+    public static final int V3_SIG_NO_CERTIFICATES = 15;
+    /* Failed to parse the V3 signer's digest record */
+    public static final int V3_SIG_MALFORMED_DIGEST = 16;
+    /* The source stamp signer contained no signatures */
+    public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
+    /* The source stamp signer's certificate could not be parsed */
+    public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
+    /* The source stamp contains a signature produced using an unknown algorithm */
+    public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
+    /* Failed to parse the signer's signature in the source stamp signature block */
+    public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
+    /* The source stamp's signature block failed verification */
+    public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
+    /* An exception was encountered when verifying the source stamp */
+    public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
+    /* The certificate digest in the APK does not match the expected digest */
+    public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
+    /*
+     * The APK contains a source stamp signature block without a corresponding stamp certificate
+     * digest in the APK contents.
+     */
+    public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
+    /*
+     * The APK does not contain the source stamp certificate digest file nor the source stamp
+     * signature block.
+     */
+    public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
+    /*
+     * None of the signatures provided by the source stamp were produced with a known signature
+     * algorithm.
+     */
+    public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
+    /*
+     * The source stamp signer's certificate in the signing block does not match the certificate in
+     * the APK.
+     */
+    public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
+    /* The APK could not be properly parsed due to a ZIP or APK format exception */
+    public static final int MALFORMED_APK = 28;
+    /* An unexpected exception was caught when attempting to verify the APK's signatures */
+    public static final int UNEXPECTED_EXCEPTION = 29;
+    /* The APK contains the certificate digest file but does not contain a stamp signature block */
+    public static final int SOURCE_STAMP_SIG_MISSING = 30;
+    /* Source stamp block contains a malformed attribute. */
+    public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
+    /* Source stamp block contains an unknown attribute. */
+    public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
+    /**
+     * Failed to parse the SigningCertificateLineage structure in the source stamp
+     * attributes section.
+     */
+    public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
+    /**
+     * The source stamp certificate does not match the terminal node in the provided
+     * proof-of-rotation structure describing the stamp certificate history.
+     */
+    public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
+    /**
+     * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+     * with signature(s) that did not verify.
+     */
+    public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
+    /** No V1 / jar signing signature blocks were found in the APK. */
+    public static final int JAR_SIG_NO_SIGNATURES = 36;
+    /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
+    public static final int JAR_SIG_PARSE_EXCEPTION = 37;
+    /** The source stamp timestamp attribute has an invalid value. */
+    public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
+
+    private final int mIssueId;
+    private final String mFormat;
+    private final Object[] mParams;
+
+    /**
+     * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
+     * {@code params}.
+     */
+    public ApkVerificationIssue(String format, Object... params) {
+        mIssueId = -1;
+        mFormat = format;
+        mParams = params;
+    }
+
+    /**
+     * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
+     * params}.
+     */
+    public ApkVerificationIssue(int issueId, Object... params) {
+        mIssueId = issueId;
+        mFormat = null;
+        mParams = params;
+    }
+
+    /**
+     * Returns the numeric ID for this issue.
+     */
+    public int getIssueId() {
+        return mIssueId;
+    }
+
+    /**
+     * Returns the optional parameters for this issue.
+     */
+    public Object[] getParams() {
+        return mParams;
+    }
+
+    @Override
+    public String toString() {
+        // If this instance was created by a subclass with a format string then return the same
+        // formatted String as the subclass.
+        if (mFormat != null) {
+            return String.format(mFormat, mParams);
+        }
+        StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
+        for (Object param : mParams) {
+            result.append(", ").append(param.toString());
+        }
+        return result.toString();
+    }
+}

+ 3657 - 0
platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java

@@ -0,0 +1,3657 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest;
+import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_SOURCE_STAMP;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+
+import com.android.apksig.ApkVerifier.Result.V2SchemeSignerInfo;
+import com.android.apksig.ApkVerifier.Result.V3SchemeSignerInfo;
+import com.android.apksig.SigningCertificateLineage.SignerConfig;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.Result.SignerInfo.ContentDigest;
+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.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK signature verifier which mimics the behavior of the Android platform.
+ *
+ * <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
+ * the verifier to be used for checking whether an APK's signatures are expected to verify on
+ * Android.
+ *
+ * <p>Use {@link Builder} to obtain instances of this verifier.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class ApkVerifier {
+
+    private static final Set<Issue> LINEAGE_RELATED_ISSUES = new HashSet<>(Arrays.asList(
+        Issue.V3_SIG_MALFORMED_LINEAGE, Issue.V3_INCONSISTENT_LINEAGES,
+        Issue.V3_SIG_POR_DID_NOT_VERIFY, Issue.V3_SIG_POR_CERT_MISMATCH));
+
+    private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
+            loadSupportedApkSigSchemeNames();
+
+    private static Map<Integer, String> loadSupportedApkSigSchemeNames() {
+        Map<Integer, String> supportedMap = new HashMap<>(2);
+        supportedMap.put(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2");
+        supportedMap.put(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3");
+        return supportedMap;
+    }
+
+    private final File mApkFile;
+    private final DataSource mApkDataSource;
+    private final File mV4SignatureFile;
+
+    private final Integer mMinSdkVersion;
+    private final int mMaxSdkVersion;
+
+    private ApkVerifier(
+            File apkFile,
+            DataSource apkDataSource,
+            File v4SignatureFile,
+            Integer minSdkVersion,
+            int maxSdkVersion) {
+        mApkFile = apkFile;
+        mApkDataSource = apkDataSource;
+        mV4SignatureFile = v4SignatureFile;
+        mMinSdkVersion = minSdkVersion;
+        mMaxSdkVersion = maxSdkVersion;
+    }
+
+    /**
+     * Verifies the APK's signatures and returns the result of verification. The APK can be
+     * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+     * The verification result also includes errors, warnings, and information about signers such
+     * as their signing certificates.
+     *
+     * <p>Verification succeeds iff the APK's signature is expected to verify on all Android
+     * platform versions specified via the {@link Builder}. If the APK's signature is expected to
+     * not verify on any of the specified platform versions, this method returns a result with one
+     * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method
+     * throws an exception.
+     *
+     * @throws IOException              if an I/O error is encountered while reading the APK
+     * @throws ApkFormatException       if the APK is malformed
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *                                  required cryptographic algorithm implementation is missing
+     * @throws IllegalStateException    if this verifier's configuration is missing required
+     *                                  information.
+     */
+    public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException,
+            IllegalStateException {
+        Closeable in = null;
+        try {
+            DataSource apk;
+            if (mApkDataSource != null) {
+                apk = mApkDataSource;
+            } else if (mApkFile != null) {
+                RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+                in = f;
+                apk = DataSources.asDataSource(f, 0, f.length());
+            } else {
+                throw new IllegalStateException("APK not provided");
+            }
+            return verify(apk);
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+    }
+
+    /**
+     * Verifies the APK's signatures and returns the result of verification. The APK can be
+     * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+     * The verification result also includes errors, warnings, and information about signers.
+     *
+     * @param apk APK file contents
+     * @throws IOException              if an I/O error is encountered while reading the APK
+     * @throws ApkFormatException       if the APK is malformed
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *                                  required cryptographic algorithm implementation is missing
+     */
+    private Result verify(DataSource apk)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
+        int maxSdkVersion = mMaxSdkVersion;
+
+        ApkUtils.ZipSections zipSections;
+        try {
+            zipSections = ApkUtils.findZipSections(apk);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+        }
+
+        ByteBuffer androidManifest = null;
+
+        int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+        Result result = new Result();
+        Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+                new HashMap<>();
+
+        // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
+        // name, but the verifiers use this parameter as the schemes supported by the target SDK
+        // range. Since the code below skips signature verification based on max SDK the mapping of
+        // supported schemes needs to be modified to ensure the verifiers do not report a stripped
+        // signature for an SDK range that does not support that signature version. For instance an
+        // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature
+        // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2
+        // verification is performed it would see the stripping protection attribute, see that V3
+        // is in the list of supported signatures, and report a stripped signature.
+        Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion);
+
+        // Android N and newer attempts to verify APKs using the APK Signing Block, which can
+        // include v2 and/or v3 signatures.  If none is found, it falls back to JAR signature
+        // verification. If the signature is found but does not verify, the APK is rejected.
+        Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+        if (maxSdkVersion >= AndroidSdkVersion.N) {
+            RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
+            // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0
+            // also includes stripping protection for the minimum SDK version on which the rotated
+            // signing key should be used.
+            int rotationMinSdkVersion = 0;
+            if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
+                try {
+                    ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk,
+                            zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT),
+                            maxSdkVersion)
+                            .setRunnablesExecutor(executor)
+                            .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+                            .build()
+                            .verify();
+                    foundApkSigSchemeIds.add(VERSION_APK_SIGNATURE_SCHEME_V31);
+                    rotationMinSdkVersion = v31Result.signers.stream().mapToInt(
+                            signer -> signer.minSdkVersion).min().orElse(0);
+                    result.mergeFrom(v31Result);
+                    signatureSchemeApkContentDigests.put(
+                            VERSION_APK_SIGNATURE_SCHEME_V31,
+                            getApkContentDigestsFromSigningSchemeResult(v31Result));
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                    // v3.1 signature not required
+                }
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
+            // Android P and newer attempts to verify APKs using APK Signature Scheme v3; since a
+            // V3.1 block should only be written with a V3.0 block, always perform the V3.0 check
+            // if the minSdkVersion supports V3.0.
+            if (maxSdkVersion >= AndroidSdkVersion.P) {
+                try {
+                    V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
+                            zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
+                            maxSdkVersion)
+                            .setRunnablesExecutor(executor)
+                            .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+                    if (rotationMinSdkVersion > 0) {
+                        builder.setRotationMinSdkVersion(rotationMinSdkVersion);
+                    }
+                    ApkSigningBlockUtils.Result v3Result = builder.build().verify();
+                    foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+                    result.mergeFrom(v3Result);
+                    signatureSchemeApkContentDigests.put(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
+                            getApkContentDigestsFromSigningSchemeResult(v3Result));
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                    // v3 signature not required unless a v3.1 signature was found as a v3.1
+                    // signature is intended to support key rotation on T+ with the v3 signature
+                    // containing the original signing key.
+                    if (foundApkSigSchemeIds.contains(
+                            VERSION_APK_SIGNATURE_SCHEME_V31)) {
+                        result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+                    }
+                }
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
+
+            // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P
+            // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or
+            // APK Signature Scheme v2 signatures.  Android P onwards verifies v2 signatures only if
+            // no APK Signature Scheme v3 (or newer scheme) signatures were found.
+            if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) {
+                try {
+                    ApkSigningBlockUtils.Result v2Result =
+                            V2SchemeVerifier.verify(
+                                    executor,
+                                    apk,
+                                    zipSections,
+                                    supportedSchemeNames,
+                                    foundApkSigSchemeIds,
+                                    Math.max(minSdkVersion, AndroidSdkVersion.N),
+                                    maxSdkVersion);
+                    foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+                    result.mergeFrom(v2Result);
+                    signatureSchemeApkContentDigests.put(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+                            getApkContentDigestsFromSigningSchemeResult(v2Result));
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                    // v2 signature not required
+                }
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
+
+            // If v4 file is specified, use additional verification on it
+            if (mV4SignatureFile != null) {
+                final ApkSigningBlockUtils.Result v4Result =
+                        V4SchemeVerifier.verify(apk, mV4SignatureFile);
+                foundApkSigSchemeIds.add(
+                        ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+                result.mergeFrom(v4Result);
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
+        }
+
+        // Android O and newer requires that APKs targeting security sandbox version 2 and higher
+        // are signed using APK Signature Scheme v2 or newer.
+        if (maxSdkVersion >= AndroidSdkVersion.O) {
+            if (androidManifest == null) {
+                androidManifest = getAndroidManifestFromApk(apk, zipSections);
+            }
+            int targetSandboxVersion =
+                    getTargetSandboxVersionFromBinaryAndroidManifest(androidManifest.slice());
+            if (targetSandboxVersion > 1) {
+                if (foundApkSigSchemeIds.isEmpty()) {
+                    result.addError(
+                            Issue.NO_SIG_FOR_TARGET_SANDBOX_VERSION,
+                            targetSandboxVersion);
+                }
+            }
+        }
+
+        List<CentralDirectoryRecord> cdRecords =
+                V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+
+        // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
+        // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
+        // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
+        // scheme) signatures were found.
+        if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
+            V1SchemeVerifier.Result v1Result =
+                    V1SchemeVerifier.verify(
+                            apk,
+                            zipSections,
+                            supportedSchemeNames,
+                            foundApkSigSchemeIds,
+                            minSdkVersion,
+                            maxSdkVersion);
+            result.mergeFrom(v1Result);
+            signatureSchemeApkContentDigests.put(
+                    ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME,
+                    getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+        }
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Verify the SourceStamp, if found in the APK.
+        try {
+            CentralDirectoryRecord sourceStampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+                        cdRecord.getName())) {
+                    sourceStampCdRecord = cdRecord;
+                    break;
+                }
+            }
+            // If SourceStamp file is found inside the APK, there must be a SourceStamp
+            // block in the APK signing block as well.
+            if (sourceStampCdRecord != null) {
+                byte[] sourceStampCertificateDigest =
+                        LocalFileRecord.getUncompressedData(
+                                apk,
+                                sourceStampCdRecord,
+                                zipSections.getZipCentralDirectoryOffset());
+                ApkSigResult sourceStampResult =
+                        V2SourceStampVerifier.verify(
+                                apk,
+                                zipSections,
+                                sourceStampCertificateDigest,
+                                signatureSchemeApkContentDigests,
+                                Math.max(minSdkVersion, AndroidSdkVersion.R),
+                                maxSdkVersion);
+                result.mergeFrom(sourceStampResult);
+            }
+        } catch (SignatureNotFoundException ignored) {
+            result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read APK", e);
+        }
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2
+        // signatures verified.
+        if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) {
+            ArrayList<Result.V1SchemeSignerInfo> v1Signers =
+                    new ArrayList<>(result.getV1SchemeSigners());
+            ArrayList<Result.V2SchemeSignerInfo> v2Signers =
+                    new ArrayList<>(result.getV2SchemeSigners());
+            ArrayList<ByteArray> v1SignerCerts = new ArrayList<>();
+            ArrayList<ByteArray> v2SignerCerts = new ArrayList<>();
+            for (Result.V1SchemeSignerInfo signer : v1Signers) {
+                try {
+                    v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+                } catch (CertificateEncodingException e) {
+                    throw new IllegalStateException(
+                            "Failed to encode JAR signer " + signer.getName() + " certs", e);
+                }
+            }
+            for (Result.V2SchemeSignerInfo signer : v2Signers) {
+                try {
+                    v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+                } catch (CertificateEncodingException e) {
+                    throw new IllegalStateException(
+                            "Failed to encode APK Signature Scheme v2 signer (index: "
+                                    + signer.getIndex() + ") certs",
+                            e);
+                }
+            }
+
+            for (int i = 0; i < v1SignerCerts.size(); i++) {
+                ByteArray v1Cert = v1SignerCerts.get(i);
+                if (!v2SignerCerts.contains(v1Cert)) {
+                    Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i);
+                    v1Signer.addError(Issue.V2_SIG_MISSING);
+                    break;
+                }
+            }
+            for (int i = 0; i < v2SignerCerts.size(); i++) {
+                ByteArray v2Cert = v2SignerCerts.get(i);
+                if (!v1SignerCerts.contains(v2Cert)) {
+                    Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i);
+                    v2Signer.addError(Issue.JAR_SIG_MISSING);
+                    break;
+                }
+            }
+        }
+
+        // If there is a v3 scheme signer and an earlier scheme signer, make sure that there is a
+        // match, or in the event of signing certificate rotation, that the v1/v2 scheme signer
+        // matches the oldest signing certificate in the provided SigningCertificateLineage
+        if (result.isVerifiedUsingV3Scheme()
+                && (result.isVerifiedUsingV1Scheme() || result.isVerifiedUsingV2Scheme())) {
+            SigningCertificateLineage lineage = result.getSigningCertificateLineage();
+            X509Certificate oldSignerCert;
+            if (result.isVerifiedUsingV1Scheme()) {
+                List<Result.V1SchemeSignerInfo> v1Signers = result.getV1SchemeSigners();
+                if (v1Signers.size() != 1) {
+                    // APK Signature Scheme v3 only supports single-signers, error to sign with
+                    // multiple and then only one
+                    result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS);
+                }
+                oldSignerCert = v1Signers.get(0).mCertChain.get(0);
+            } else {
+                List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
+                if (v2Signers.size() != 1) {
+                    // APK Signature Scheme v3 only supports single-signers, error to sign with
+                    // multiple and then only one
+                    result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS);
+                }
+                oldSignerCert = v2Signers.get(0).mCerts.get(0);
+            }
+            if (lineage == null) {
+                // no signing certificate history with which to contend, just make sure that v3
+                // matches previous versions
+                List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
+                if (v3Signers.size() != 1) {
+                    // multiple v3 signers should never exist without rotation history, since
+                    // multiple signers implies a different signer for different platform versions
+                    result.addError(Issue.V3_SIG_MULTIPLE_SIGNERS);
+                }
+                try {
+                    if (!Arrays.equals(oldSignerCert.getEncoded(),
+                            v3Signers.get(0).mCerts.get(0).getEncoded())) {
+                        result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+                    }
+                } catch (CertificateEncodingException e) {
+                    // we just go the encoding for the v1/v2 certs above, so must be v3
+                    throw new RuntimeException(
+                            "Failed to encode APK Signature Scheme v3 signer cert", e);
+                }
+            } else {
+                // we have some signing history, make sure that the root of the history is the same
+                // as our v1/v2 signer
+                try {
+                    lineage = lineage.getSubLineage(oldSignerCert);
+                    if (lineage.size() != 1) {
+                        // the v1/v2 signer was found, but not at the root of the lineage
+                        result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+                    }
+                } catch (IllegalArgumentException e) {
+                    // the v1/v2 signer was not found in the lineage
+                    result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+                }
+            }
+        }
+
+
+        // If there is a v4 scheme signer, make sure that their certificates match.
+        // The apkDigest field in the v4 signature should match the selected v2/v3.
+        if (result.isVerifiedUsingV4Scheme()) {
+            List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners();
+
+            List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
+                    v4Signers.get(0).getContentDigests();
+            if (digestsFromV4.size() != 1) {
+                result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV4.size());
+                if (digestsFromV4.isEmpty()) {
+                    return result;
+                }
+            }
+            final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
+
+            if (result.isVerifiedUsingV3Scheme()) {
+                final boolean isV31 = result.isVerifiedUsingV31Scheme();
+                final int expectedSize = isV31 ? 2 : 1;
+                if (v4Signers.size() != expectedSize) {
+                    result.addError(isV31 ? Issue.V41_SIG_NEEDS_TWO_SIGNERS
+                            : Issue.V4_SIG_MULTIPLE_SIGNERS);
+                    return result;
+                }
+
+                checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4,
+                        result);
+                if (isV31) {
+                    List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV41 =
+                            v4Signers.get(1).getContentDigests();
+                    if (digestsFromV41.size() != 1) {
+                        result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV41.size());
+                        if (digestsFromV41.isEmpty()) {
+                            return result;
+                        }
+                    }
+                    final byte[] digestFromV41 = digestsFromV41.get(0).getValue();
+                    checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts,
+                            digestFromV41, result);
+                }
+            } else if (result.isVerifiedUsingV2Scheme()) {
+                if (v4Signers.size() != 1) {
+                    result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+                }
+
+                List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
+                if (v2Signers.size() != 1) {
+                    result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+                }
+
+                // Compare certificates.
+                checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result);
+
+                // Compare digests.
+                final byte[] digestFromV2 = pickBestDigestForV4(
+                        v2Signers.get(0).getContentDigests());
+                if (!Arrays.equals(digestFromV4, digestFromV2)) {
+                    result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 2, toHex(digestFromV2),
+                            toHex(digestFromV4));
+                }
+            } else {
+                throw new RuntimeException("V4 signature must be also verified with V2/V3");
+            }
+        }
+
+        // If the targetSdkVersion has a minimum required signature scheme version then verify
+        // that the APK was signed with at least that version.
+        try {
+            if (androidManifest == null) {
+                androidManifest = getAndroidManifestFromApk(apk, zipSections);
+            }
+        } catch (ApkFormatException e) {
+            // If the manifest is not available then skip the minimum signature scheme requirement
+            // to support bundle verification.
+        }
+        if (androidManifest != null) {
+            int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
+                    androidManifest.slice());
+            int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
+            // The platform currently only enforces a single minimum signature scheme version, but
+            // when later platform versions support another minimum version this will need to be
+            // expanded to verify the minimum based on the target and maximum SDK version.
+            if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME
+                    && maxSdkVersion >= targetSdkVersion) {
+                switch (minSchemeVersion) {
+                    case VERSION_APK_SIGNATURE_SCHEME_V2:
+                        if (result.isVerifiedUsingV2Scheme()) {
+                            break;
+                        }
+                        // Allow this case to fall through to the next as a signature satisfying a
+                        // later scheme version will also satisfy this requirement.
+                    case VERSION_APK_SIGNATURE_SCHEME_V3:
+                        if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) {
+                            break;
+                        }
+                        result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
+                                targetSdkVersion,
+                                minSchemeVersion);
+                }
+            }
+        }
+
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Verified
+        result.setVerified();
+        if (result.isVerifiedUsingV31Scheme()) {
+            List<Result.V3SchemeSignerInfo> v31Signers = result.getV31SchemeSigners();
+            result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate());
+        } else if (result.isVerifiedUsingV3Scheme()) {
+            List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
+            result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate());
+        } else if (result.isVerifiedUsingV2Scheme()) {
+            for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+                result.addSignerCertificate(signerInfo.getCertificate());
+            }
+        } else if (result.isVerifiedUsingV1Scheme()) {
+            for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) {
+                result.addSignerCertificate(signerInfo.getCertificate());
+            }
+        } else {
+            throw new RuntimeException(
+                    "APK verified, but has not verified using any of v1, v2 or v3 schemes");
+        }
+
+        return result;
+    }
+
+    /**
+     * Verifies and returns the minimum SDK version, either as provided to the builder or as read
+     * from the {@code apk}'s AndroidManifest.xml.
+     */
+    private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections)
+            throws ApkFormatException, IOException {
+        if (mMinSdkVersion != null) {
+            if (mMinSdkVersion < 0) {
+                throw new IllegalArgumentException(
+                        "minSdkVersion must not be negative: " + mMinSdkVersion);
+            }
+            if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
+                throw new IllegalArgumentException(
+                        "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
+                                + ")");
+            }
+            return mMinSdkVersion;
+        }
+
+        ByteBuffer androidManifest = null;
+        // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
+        if (androidManifest == null) {
+            androidManifest = getAndroidManifestFromApk(apk, zipSections);
+        }
+        int minSdkVersion =
+                ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
+        if (minSdkVersion > mMaxSdkVersion) {
+            throw new IllegalArgumentException(
+                    "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+                            + mMaxSdkVersion + ")");
+        }
+        return minSdkVersion;
+    }
+
+    /**
+     * Returns the mapping of signature scheme version to signature scheme name for all signature
+     * schemes starting from V2 supported by the {@code maxSdkVersion}.
+     */
+    private static Map<Integer, String> getSupportedSchemeNames(int maxSdkVersion) {
+        Map<Integer, String> supportedSchemeNames;
+        if (maxSdkVersion >= AndroidSdkVersion.P) {
+            supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
+        } else if (maxSdkVersion >= AndroidSdkVersion.N) {
+            supportedSchemeNames = new HashMap<>(1);
+            supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+                    SUPPORTED_APK_SIG_SCHEME_NAMES.get(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+        } else {
+            supportedSchemeNames = Collections.emptyMap();
+        }
+        return supportedSchemeNames;
+    }
+
+    /**
+     * Verifies the APK's source stamp signature and returns the result of the verification.
+     *
+     * <p>The APK's source stamp can be considered verified if the result's {@link
+     * Result#isVerified} returns {@code true}. The details of the source stamp verification can
+     * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or
+     * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the
+     * verification fails additional details regarding the failure can be obtained from {@link
+     * Result#getAllErrors()}}.
+     */
+    public Result verifySourceStamp() {
+        return verifySourceStamp(null);
+    }
+
+    /**
+     * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+     * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+     * of the verification.
+     *
+     * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+     * if present, without verifying the actual source stamp certificate used to sign the source
+     * stamp. This can be used to verify an APK contains a properly signed source stamp without
+     * verifying a particular signer.
+     *
+     * @see #verifySourceStamp()
+     */
+    public Result verifySourceStamp(String expectedCertDigest) {
+        Closeable in = null;
+        try {
+            DataSource apk;
+            if (mApkDataSource != null) {
+                apk = mApkDataSource;
+            } else if (mApkFile != null) {
+                RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+                in = f;
+                apk = DataSources.asDataSource(f, 0, f.length());
+            } else {
+                throw new IllegalStateException("APK not provided");
+            }
+            return verifySourceStamp(apk, expectedCertDigest);
+        } catch (IOException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                    Issue.UNEXPECTED_EXCEPTION, e);
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Compares the digests coming from signature blocks. Returns {@code true} if at least one
+     * digest algorithm is present in both digests and actual digests for all common algorithms
+     * are the same.
+     */
+    public static boolean compareDigests(
+            Map<ContentDigestAlgorithm, byte[]> firstDigests,
+            Map<ContentDigestAlgorithm, byte[]> secondDigests) throws NoSuchAlgorithmException {
+
+        Set<ContentDigestAlgorithm> intersectKeys = new HashSet<>(firstDigests.keySet());
+        intersectKeys.retainAll(secondDigests.keySet());
+        if (intersectKeys.isEmpty()) {
+            return false;
+        }
+
+        for (ContentDigestAlgorithm algorithm : intersectKeys) {
+            if (!Arrays.equals(firstDigests.get(algorithm),
+                    secondDigests.get(algorithm))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+     * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+     * returns the result of the verification.
+     *
+     * @see #verifySourceStamp(String)
+     */
+    private Result verifySourceStamp(DataSource apk, String expectedCertDigest) {
+        try {
+            ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+            int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+            // Attempt to obtain the source stamp's certificate digest from the APK.
+            List<CentralDirectoryRecord> cdRecords =
+                    V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+            CentralDirectoryRecord sourceStampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+                    sourceStampCdRecord = cdRecord;
+                    break;
+                }
+            }
+
+            // If the source stamp's certificate digest is not available within the APK then the
+            // source stamp cannot be verified; check if a source stamp signing block is in the
+            // APK's signature block to determine the appropriate status to return.
+            if (sourceStampCdRecord == null) {
+                boolean stampSigningBlockFound;
+                try {
+                    ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                            VERSION_SOURCE_STAMP);
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result);
+                    stampSigningBlockFound = true;
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+                    stampSigningBlockFound = false;
+                }
+                if (stampSigningBlockFound) {
+                    return createSourceStampResultWithError(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+                            Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+                } else {
+                    return createSourceStampResultWithError(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING,
+                            Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+                }
+            }
+
+            // Verify that the contents of the source stamp certificate digest match the expected
+            // value, if provided.
+            byte[] sourceStampCertificateDigest =
+                    LocalFileRecord.getUncompressedData(
+                            apk,
+                            sourceStampCdRecord,
+                            zipSections.getZipCentralDirectoryOffset());
+            if (expectedCertDigest != null) {
+                String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest);
+                if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+                    return createSourceStampResultWithError(
+                            Result.SourceStampInfo.SourceStampVerificationStatus
+                                    .CERT_DIGEST_MISMATCH,
+                            Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest,
+                            expectedCertDigest);
+                }
+            }
+
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+                    new HashMap<>();
+            Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion);
+            Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+            Result result = new Result();
+            ApkSigningBlockUtils.Result v3Result = null;
+            if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+                v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+                        supportedSchemeNames, signatureSchemeApkContentDigests,
+                        VERSION_APK_SIGNATURE_SCHEME_V3,
+                        Math.max(minSdkVersion, AndroidSdkVersion.P));
+                if (v3Result != null && v3Result.containsErrors()) {
+                    result.mergeFrom(v3Result);
+                    return mergeSourceStampResult(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                            result);
+                }
+            }
+
+            ApkSigningBlockUtils.Result v2Result = null;
+            if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P
+                    || foundApkSigSchemeIds.isEmpty())) {
+                v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+                        supportedSchemeNames, signatureSchemeApkContentDigests,
+                        VERSION_APK_SIGNATURE_SCHEME_V2,
+                        Math.max(minSdkVersion, AndroidSdkVersion.N));
+                if (v2Result != null && v2Result.containsErrors()) {
+                    result.mergeFrom(v2Result);
+                    return mergeSourceStampResult(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                            result);
+                }
+            }
+
+            if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) {
+                signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+                        getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+            }
+
+            ApkSigResult sourceStampResult =
+                    V2SourceStampVerifier.verify(
+                            apk,
+                            zipSections,
+                            sourceStampCertificateDigest,
+                            signatureSchemeApkContentDigests,
+                            minSdkVersion,
+                            mMaxSdkVersion);
+            result.mergeFrom(sourceStampResult);
+            // Since the caller is only seeking to verify the source stamp the Result can be marked
+            // as verified if the source stamp verification was successful.
+            if (sourceStampResult.verified) {
+                result.setVerified();
+            } else {
+                // To prevent APK signature verification with a failed / missing source stamp the
+                // source stamp verification will only log warnings; to allow the caller to capture
+                // the failure reason treat all warnings as errors.
+                result.setWarningsAsErrors(true);
+            }
+            return result;
+        } catch (ApkFormatException | IOException | ZipFormatException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                    Issue.MALFORMED_APK, e);
+        } catch (NoSuchAlgorithmException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                    Issue.UNEXPECTED_EXCEPTION, e);
+        } catch (SignatureNotFoundException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+                    Issue.SOURCE_STAMP_SIG_MISSING);
+        }
+    }
+
+    /**
+     * Creates and returns a {@code Result} that can be returned for source stamp verification
+     * with the provided source stamp {@code verificationStatus}, and logs an error for the
+     * specified {@code issue} and {@code params}.
+     */
+    private static Result createSourceStampResultWithError(
+            Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue,
+            Object... params) {
+        Result result = new Result();
+        result.addError(issue, params);
+        return mergeSourceStampResult(verificationStatus, result);
+    }
+
+    /**
+     * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the
+     * source stamp status to the provided {@code verificationStatus}.
+     */
+    private static Result mergeSourceStampResult(
+            Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus,
+            Result result) {
+        result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus);
+        return result;
+    }
+
+    /**
+     * Gets content digests, signing lineage and certificates from the given {@code schemeId} block
+     * alongside encountered errors info and creates a new {@code Result} containing all this
+     * information.
+     */
+    public static Result getSigningBlockResult(
+        DataSource apk, ApkUtils.ZipSections zipSections, int sdkVersion, int schemeId)
+        throws IOException, NoSuchAlgorithmException{
+        Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests =
+                new HashMap<>();
+        Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(sdkVersion);
+        Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+        Result result = new Result();
+        result.mergeFrom(getApkContentDigests(apk, zipSections,
+                foundApkSigSchemeIds, supportedSchemeNames, sigSchemeApkContentDigests,
+                schemeId, sdkVersion, sdkVersion));
+        return result;
+    }
+
+    /**
+     * Gets the content digest from the {@code result}'s signers. Ignores {@code ContentDigest}s
+     * for which {@code SignatureAlgorithm} is {@code null}.
+     */
+    public static Map<ContentDigestAlgorithm, byte[]> getContentDigestsFromResult(
+        Result result, int schemeId) {
+        Map<ContentDigestAlgorithm, byte[]>  apkContentDigests = new HashMap<>();
+        if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V2
+                || schemeId == VERSION_APK_SIGNATURE_SCHEME_V3
+                || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)) {
+            return apkContentDigests;
+        }
+        switch (schemeId) {
+            case VERSION_APK_SIGNATURE_SCHEME_V2:
+                for (V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+                    getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+                }
+                break;
+            case VERSION_APK_SIGNATURE_SCHEME_V3:
+                for (Result.V3SchemeSignerInfo signerInfo : result.getV3SchemeSigners()) {
+                    getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+                }
+                break;
+            case  VERSION_APK_SIGNATURE_SCHEME_V31:
+                for (Result.V3SchemeSignerInfo signerInfo : result.getV31SchemeSigners()) {
+                    getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+                }
+                break;
+        }
+        return apkContentDigests;
+    }
+
+    private static void getContentDigests(
+            List<ContentDigest> digests, Map<ContentDigestAlgorithm, byte[]> contentDigestsMap) {
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+            digests) {
+            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+                    contentDigest.getSignatureAlgorithmId());
+            if (signatureAlgorithm == null) {
+                continue;
+            }
+            contentDigestsMap.put(signatureAlgorithm.getContentDigestAlgorithm(),
+                    contentDigest.getValue());
+        }
+    }
+
+    /**
+     * Checks whether a given {@code result} contains errors indicating that a signing certificate
+     * lineage is incorrect.
+     */
+    public static boolean containsLineageErrors(
+        Result result) {
+        if (!result.containsErrors()) {
+            return false;
+        }
+
+        return (result.getAllErrors().stream().map(i -> i.getIssue())
+                .anyMatch(error -> LINEAGE_RELATED_ISSUES.contains(error)));
+    }
+
+
+    /**
+     * Gets a lineage from the first signer from a given {@code result}.
+     * If the {@code result} contains errors related to the lineage incorrectness or there are no
+     * signers or certificates, it returns {@code null}.
+     * If the lineage is empty but there is a signer, it returns a 1-element lineage containing
+     * the signing key.
+     */
+    public static SigningCertificateLineage getLineageFromResult(
+        Result result, int sdkVersion, int schemeId)
+        throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+        SignatureException {
+        if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V3
+                        || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)
+                || containsLineageErrors(result)) {
+            return null;
+        }
+        List<V3SchemeSignerInfo> signersInfo =
+                schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 ?
+                        result.getV3SchemeSigners() : result.getV31SchemeSigners();
+        if (signersInfo.isEmpty()) {
+            return null;
+        }
+        V3SchemeSignerInfo firstSignerInfo = signersInfo.get(0);
+        SigningCertificateLineage lineage = firstSignerInfo.mSigningCertificateLineage;
+        if (lineage == null && firstSignerInfo.getCertificate() != null) {
+            try {
+                lineage = new SigningCertificateLineage.Builder(
+                        new SignerConfig.Builder(
+                                /* privateKey= */ null, firstSignerInfo.getCertificate())
+                                .build()).build();
+            } catch (Exception e) {
+                return null;
+            }
+        }
+        return lineage;
+    }
+
+    /**
+     * Obtains the APK content digest(s) and adds them to the provided {@code
+     * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+     * merged with a {@code Result} to notify the client of any errors.
+     *
+     * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the
+     * content digests for V1 signatures use {@link
+     * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+     * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+     * returned.
+     */
+    private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+            ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds,
+            Map<Integer, String> supportedSchemeNames,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
+            int apkSigSchemeVersion, int minSdkVersion)
+            throws IOException, NoSuchAlgorithmException {
+        return getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames,
+                sigSchemeApkContentDigests, apkSigSchemeVersion, minSdkVersion, mMaxSdkVersion);
+    }
+
+
+    /**
+     * Obtains the APK content digest(s) and adds them to the provided {@code
+     * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+     * merged with a {@code Result} to notify the client of any errors.
+     *
+     * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the
+     * content digests for V1 signatures use {@link
+     * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+     * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+     * returned.
+     */
+    private static ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+            ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds,
+            Map<Integer, String> supportedSchemeNames,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
+            int apkSigSchemeVersion, int minSdkVersion, int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException {
+        if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2
+                || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3
+                || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V31)) {
+            return null;
+        }
+        ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion);
+        SignatureInfo signatureInfo;
+        try {
+            int sigSchemeBlockId;
+            switch (apkSigSchemeVersion) {
+                case VERSION_APK_SIGNATURE_SCHEME_V31:
+                    sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+                    break;
+                case VERSION_APK_SIGNATURE_SCHEME_V3:
+                    sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+                    break;
+                default:
+                    sigSchemeBlockId =
+                        V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+            }
+            signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
+                    sigSchemeBlockId, result);
+        } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+            return null;
+        }
+        foundApkSigSchemeIds.add(apkSigSchemeVersion);
+
+        Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+            V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+                    contentDigestsToVerify, supportedSchemeNames,
+                    foundApkSigSchemeIds, minSdkVersion, maxSdkVersion, result);
+        } else {
+            V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+                    contentDigestsToVerify, result);
+        }
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+                ContentDigestAlgorithm.class);
+        for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) {
+            for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+                    signerInfo.contentDigests) {
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+                        contentDigest.getSignatureAlgorithmId());
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(),
+                        contentDigest.getValue());
+            }
+        }
+        sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests);
+        return result;
+    }
+
+    private static void checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers,
+            List<X509Certificate> v4Certs, byte[] digestFromV4, Result result) {
+        if (v3Signers.size() != 1) {
+            result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+        }
+
+        // Compare certificates.
+        checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result);
+
+        // Compare digests.
+        final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests());
+        if (!Arrays.equals(digestFromV4, digestFromV3)) {
+            result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 3, toHex(digestFromV3),
+                    toHex(digestFromV4));
+        }
+    }
+
+    private static void checkV4Certificate(List<X509Certificate> v4Certs,
+            List<X509Certificate> v2v3Certs, Result result) {
+        try {
+            byte[] v4Cert = v4Certs.get(0).getEncoded();
+            byte[] cert = v2v3Certs.get(0).getEncoded();
+            if (!Arrays.equals(cert, v4Cert)) {
+                result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH);
+            }
+        } catch (CertificateEncodingException e) {
+            throw new RuntimeException("Failed to encode APK signer cert", e);
+        }
+    }
+
+    private static byte[] pickBestDigestForV4(
+            List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+        collectApkContentDigests(contentDigests, apkContentDigests);
+        return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
+    }
+
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult(
+            ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+        for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+            collectApkContentDigests(signerInfo.contentDigests, apkContentDigests);
+        }
+        return apkContentDigests;
+    }
+
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+            List<CentralDirectoryRecord> cdRecords,
+            DataSource apk,
+            ApkUtils.ZipSections zipSections)
+            throws IOException, ApkFormatException {
+        CentralDirectoryRecord manifestCdRecord = null;
+        Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+                ContentDigestAlgorithm.class);
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+                manifestCdRecord = cdRecord;
+                break;
+            }
+        }
+        if (manifestCdRecord == null) {
+            // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+            // digest is enough since this would affect the final digest signed by the stamp, and
+            // thus an empty digest will invalidate that signature.
+            return v1ContentDigest;
+        }
+        try {
+            byte[] manifestBytes =
+                    LocalFileRecord.getUncompressedData(
+                            apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+            v1ContentDigest.put(
+                    ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+            return v1ContentDigest;
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read APK", e);
+        }
+    }
+
+    private static void collectApkContentDigests(
+            List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+            SignatureAlgorithm signatureAlgorithm =
+                    SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+            if (signatureAlgorithm == null) {
+                continue;
+            }
+            ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+        }
+
+    }
+
+    private static ByteBuffer getAndroidManifestFromApk(
+            DataSource apk, ApkUtils.ZipSections zipSections)
+            throws IOException, ApkFormatException {
+        List<CentralDirectoryRecord> cdRecords =
+                V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+        try {
+            return ApkSigner.getAndroidManifestFromApk(
+                    cdRecords,
+                    apk.slice(0, zipSections.getZipCentralDirectoryOffset()));
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read AndroidManifest.xml", e);
+        }
+    }
+
+    private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
+        if (targetSdkVersion >= AndroidSdkVersion.R) {
+            return VERSION_APK_SIGNATURE_SCHEME_V2;
+        }
+        return VERSION_JAR_SIGNATURE_SCHEME;
+    }
+
+    /**
+     * Result of verifying an APKs signatures. The APK can be considered verified iff
+     * {@link #isVerified()} returns {@code true}.
+     */
+    public static class Result {
+        private final List<IssueWithParams> mErrors = new ArrayList<>();
+        private final List<IssueWithParams> mWarnings = new ArrayList<>();
+        private final List<X509Certificate> mSignerCerts = new ArrayList<>();
+        private final List<V1SchemeSignerInfo> mV1SchemeSigners = new ArrayList<>();
+        private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
+        private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
+        private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+        private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>();
+        private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
+        private SourceStampInfo mSourceStampInfo;
+
+        private boolean mVerified;
+        private boolean mVerifiedUsingV1Scheme;
+        private boolean mVerifiedUsingV2Scheme;
+        private boolean mVerifiedUsingV3Scheme;
+        private boolean mVerifiedUsingV31Scheme;
+        private boolean mVerifiedUsingV4Scheme;
+        private boolean mSourceStampVerified;
+        private boolean mWarningsAsErrors;
+        private SigningCertificateLineage mSigningCertificateLineage;
+
+        /**
+         * Returns {@code true} if the APK's signatures verified.
+         */
+        public boolean isVerified() {
+            return mVerified;
+        }
+
+        private void setVerified() {
+            mVerified = true;
+        }
+
+        /**
+         * Returns {@code true} if the APK's JAR signatures verified.
+         */
+        public boolean isVerifiedUsingV1Scheme() {
+            return mVerifiedUsingV1Scheme;
+        }
+
+        /**
+         * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
+         */
+        public boolean isVerifiedUsingV2Scheme() {
+            return mVerifiedUsingV2Scheme;
+        }
+
+        /**
+         * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified.
+         */
+        public boolean isVerifiedUsingV3Scheme() {
+            return mVerifiedUsingV3Scheme;
+        }
+
+        /**
+         * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified.
+         */
+        public boolean isVerifiedUsingV31Scheme() {
+            return mVerifiedUsingV31Scheme;
+        }
+
+        /**
+         * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
+         */
+        public boolean isVerifiedUsingV4Scheme() {
+            return mVerifiedUsingV4Scheme;
+        }
+
+        /**
+         * Returns {@code true} if the APK's SourceStamp signature verified.
+         */
+        public boolean isSourceStampVerified() {
+            return mSourceStampVerified;
+        }
+
+        /**
+         * Returns the verified signers' certificates, one per signer.
+         */
+        public List<X509Certificate> getSignerCertificates() {
+            return mSignerCerts;
+        }
+
+        private void addSignerCertificate(X509Certificate cert) {
+            mSignerCerts.add(cert);
+        }
+
+        /**
+         * Returns information about JAR signers associated with the APK's signature. These are the
+         * signers used by Android.
+         *
+         * @see #getV1SchemeIgnoredSigners()
+         */
+        public List<V1SchemeSignerInfo> getV1SchemeSigners() {
+            return mV1SchemeSigners;
+        }
+
+        /**
+         * Returns information about JAR signers ignored by the APK's signature verification
+         * process. These signers are ignored by Android. However, each signer's errors or warnings
+         * will contain information about why they are ignored.
+         *
+         * @see #getV1SchemeSigners()
+         */
+        public List<V1SchemeSignerInfo> getV1SchemeIgnoredSigners() {
+            return mV1SchemeIgnoredSigners;
+        }
+
+        /**
+         * Returns information about APK Signature Scheme v2 signers associated with the APK's
+         * signature.
+         */
+        public List<V2SchemeSignerInfo> getV2SchemeSigners() {
+            return mV2SchemeSigners;
+        }
+
+        /**
+         * Returns information about APK Signature Scheme v3 signers associated with the APK's
+         * signature.
+         *
+         * <note> Multiple signers represent different targeted platform versions, not
+         * a signing identity of multiple signers.  APK Signature Scheme v3 only supports single
+         * signer identities.</note>
+         */
+        public List<V3SchemeSignerInfo> getV3SchemeSigners() {
+            return mV3SchemeSigners;
+        }
+
+        /**
+         * Returns information about APK Signature Scheme v3.1 signers associated with the APK's
+         * signature.
+         *
+         * <note> Multiple signers represent different targeted platform versions, not
+         * a signing identity of multiple signers.  APK Signature Scheme v3.1 only supports single
+         * signer identities.</note>
+         */
+        public List<V3SchemeSignerInfo> getV31SchemeSigners() {
+            return mV31SchemeSigners;
+        }
+
+        /**
+         * Returns information about APK Signature Scheme v4 signers associated with the APK's
+         * signature.
+         */
+        public List<V4SchemeSignerInfo> getV4SchemeSigners() {
+            return mV4SchemeSigners;
+        }
+
+        /**
+         * Returns information about SourceStamp associated with the APK's signature.
+         */
+        public SourceStampInfo getSourceStampInfo() {
+            return mSourceStampInfo;
+        }
+
+        /**
+         * Returns the combined SigningCertificateLineage associated with this APK's APK Signature
+         * Scheme v3 signing block.
+         */
+        public SigningCertificateLineage getSigningCertificateLineage() {
+            return mSigningCertificateLineage;
+        }
+
+        void addError(Issue msg, Object... parameters) {
+            mErrors.add(new IssueWithParams(msg, parameters));
+        }
+
+        void addWarning(Issue msg, Object... parameters) {
+            mWarnings.add(new IssueWithParams(msg, parameters));
+        }
+
+        /**
+         * Sets whether warnings should be treated as errors.
+         */
+        void setWarningsAsErrors(boolean value) {
+            mWarningsAsErrors = value;
+        }
+
+        /**
+         * Returns errors encountered while verifying the APK's signatures.
+         */
+        public List<IssueWithParams> getErrors() {
+            if (!mWarningsAsErrors) {
+                return mErrors;
+            } else {
+                List<IssueWithParams> allErrors = new ArrayList<>();
+                allErrors.addAll(mErrors);
+                allErrors.addAll(mWarnings);
+                return allErrors;
+            }
+        }
+
+        /**
+         * Returns warnings encountered while verifying the APK's signatures.
+         */
+        public List<IssueWithParams> getWarnings() {
+            return mWarnings;
+        }
+
+        private void mergeFrom(V1SchemeVerifier.Result source) {
+            mVerifiedUsingV1Scheme = source.verified;
+            mErrors.addAll(source.getErrors());
+            mWarnings.addAll(source.getWarnings());
+            for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) {
+                mV1SchemeSigners.add(new V1SchemeSignerInfo(signer));
+            }
+            for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) {
+                mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer));
+            }
+        }
+
+        private void mergeFrom(ApkSigResult source) {
+            switch (source.signatureSchemeVersion) {
+                case VERSION_SOURCE_STAMP:
+                    mSourceStampVerified = source.verified;
+                    if (!source.mSigners.isEmpty()) {
+                        mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown ApkSigResult Signing Block Scheme Id "
+                                    + source.signatureSchemeVersion);
+            }
+        }
+
+        private void mergeFrom(ApkSigningBlockUtils.Result source) {
+            if (source == null) {
+                return;
+            }
+            if (source.containsErrors()) {
+                mErrors.addAll(source.getErrors());
+            }
+            if (source.containsWarnings()) {
+                mWarnings.addAll(source.getWarnings());
+            }
+            switch (source.signatureSchemeVersion) {
+                case VERSION_APK_SIGNATURE_SCHEME_V2:
+                    mVerifiedUsingV2Scheme = source.verified;
+                    for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+                        mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
+                    }
+                    break;
+                case VERSION_APK_SIGNATURE_SCHEME_V3:
+                    mVerifiedUsingV3Scheme = source.verified;
+                    for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+                        mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
+                    }
+                    // Do not overwrite a previously set lineage from a v3.1 signing block.
+                    if (mSigningCertificateLineage == null) {
+                        mSigningCertificateLineage = source.signingCertificateLineage;
+                    }
+                    break;
+                case VERSION_APK_SIGNATURE_SCHEME_V31:
+                    mVerifiedUsingV31Scheme = source.verified;
+                    for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+                        mV31SchemeSigners.add(new V3SchemeSignerInfo(signer));
+                    }
+                    mSigningCertificateLineage = source.signingCertificateLineage;
+                    break;
+                case VERSION_APK_SIGNATURE_SCHEME_V4:
+                    mVerifiedUsingV4Scheme = source.verified;
+                    for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+                        mV4SchemeSigners.add(new V4SchemeSignerInfo(signer));
+                    }
+                    break;
+                case VERSION_SOURCE_STAMP:
+                    mSourceStampVerified = source.verified;
+                    if (!source.signers.isEmpty()) {
+                        mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
+            }
+        }
+
+        /**
+         * Returns {@code true} if an error was encountered while verifying the APK. Any error
+         * prevents the APK from being considered verified.
+         */
+        public boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            if (mWarningsAsErrors && !mWarnings.isEmpty()) {
+                return true;
+            }
+            if (!mV1SchemeSigners.isEmpty()) {
+                for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
+                }
+            }
+            if (!mV2SchemeSigners.isEmpty()) {
+                for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
+                }
+            }
+            if (!mV3SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
+                }
+            }
+            if (!mV31SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV31SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
+                }
+            }
+            if (mSourceStampInfo != null) {
+                if (mSourceStampInfo.containsErrors()) {
+                    return true;
+                }
+                if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Returns all errors for this result, including any errors from signature scheme signers
+         * and the source stamp.
+         */
+        public List<IssueWithParams> getAllErrors() {
+            List<IssueWithParams> errors = new ArrayList<>();
+            errors.addAll(mErrors);
+            if (mWarningsAsErrors) {
+                errors.addAll(mWarnings);
+            }
+            if (!mV1SchemeSigners.isEmpty()) {
+                for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (!mV2SchemeSigners.isEmpty()) {
+                for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (!mV3SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (!mV31SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV31SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (mSourceStampInfo != null) {
+                errors.addAll(mSourceStampInfo.getErrors());
+                if (mWarningsAsErrors) {
+                    errors.addAll(mSourceStampInfo.getWarnings());
+                }
+            }
+            return errors;
+        }
+
+        /**
+         * Information about a JAR signer associated with the APK's signature.
+         */
+        public static class V1SchemeSignerInfo {
+            private final String mName;
+            private final List<X509Certificate> mCertChain;
+            private final String mSignatureBlockFileName;
+            private final String mSignatureFileName;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+
+            private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) {
+                mName = result.name;
+                mCertChain = result.certChain;
+                mSignatureBlockFileName = result.signatureBlockFileName;
+                mSignatureFileName = result.signatureFileName;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+            }
+
+            /**
+             * Returns a user-friendly name of the signer.
+             */
+            public String getName() {
+                return mName;
+            }
+
+            /**
+             * Returns the name of the JAR entry containing this signer's JAR signature block file.
+             */
+            public String getSignatureBlockFileName() {
+                return mSignatureBlockFileName;
+            }
+
+            /**
+             * Returns the name of the JAR entry containing this signer's JAR signature file.
+             */
+            public String getSignatureFileName() {
+                return mSignatureFileName;
+            }
+
+            /**
+             * Returns this signer's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the signer's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCertChain.isEmpty() ? null : mCertChain.get(0);
+            }
+
+            /**
+             * Returns the certificate chain for the signer's public key. The certificate containing
+             * the public key is first, followed by the certificate (if any) which issued the
+             * signing certificate, and so forth. An empty list may be returned if an error was
+             * encountered during verification (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificateChain() {
+                return mCertChain;
+            }
+
+            /**
+             * Returns {@code true} if an error was encountered while verifying this signer's JAR
+             * signature. Any error prevents the signer's signature from being considered verified.
+             */
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            /**
+             * Returns errors encountered while verifying this signer's JAR signature. Any error
+             * prevents the signer's signature from being considered verified.
+             */
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            /**
+             * Returns warnings encountered while verifying this signer's JAR signature. Warnings
+             * do not prevent the signer's signature from being considered verified.
+             */
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            private void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+        }
+
+        /**
+         * Information about an APK Signature Scheme v2 signer associated with the APK's signature.
+         */
+        public static class V2SchemeSignerInfo {
+            private final int mIndex;
+            private final List<X509Certificate> mCerts;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+            private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+                    mContentDigests;
+
+            private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+                mIndex = result.index;
+                mCerts = result.certs;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+                mContentDigests = result.contentDigests;
+            }
+
+            /**
+             * Returns this signer's {@code 0}-based index in the list of signers contained in the
+             * APK's APK Signature Scheme v2 signature.
+             */
+            public int getIndex() {
+                return mIndex;
+            }
+
+            /**
+             * Returns this signer's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the signer's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCerts.isEmpty() ? null : mCerts.get(0);
+            }
+
+            /**
+             * Returns this signer's certificates. The first certificate is for the signer's public
+             * key. An empty list may be returned if an error was encountered during verification
+             * (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificates() {
+                return mCerts;
+            }
+
+            private void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+                return mContentDigests;
+            }
+        }
+
+        /**
+         * Information about an APK Signature Scheme v3 signer associated with the APK's signature.
+         */
+        public static class V3SchemeSignerInfo {
+            private final int mIndex;
+            private final List<X509Certificate> mCerts;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+            private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+                    mContentDigests;
+            private final int mMinSdkVersion;
+            private final int mMaxSdkVersion;
+            private final boolean mRotationTargetsDevRelease;
+            private final SigningCertificateLineage mSigningCertificateLineage;
+
+            private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+                mIndex = result.index;
+                mCerts = result.certs;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+                mContentDigests = result.contentDigests;
+                mMinSdkVersion = result.minSdkVersion;
+                mMaxSdkVersion = result.maxSdkVersion;
+                mSigningCertificateLineage = result.signingCertificateLineage;
+                mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt(
+                        attribute -> attribute.getId()).anyMatch(
+                        attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+            }
+
+            /**
+             * Returns this signer's {@code 0}-based index in the list of signers contained in the
+             * APK's APK Signature Scheme v3 signature.
+             */
+            public int getIndex() {
+                return mIndex;
+            }
+
+            /**
+             * Returns this signer's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the signer's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCerts.isEmpty() ? null : mCerts.get(0);
+            }
+
+            /**
+             * Returns this signer's certificates. The first certificate is for the signer's public
+             * key. An empty list may be returned if an error was encountered during verification
+             * (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificates() {
+                return mCerts;
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+                return mContentDigests;
+            }
+
+            /**
+             * Returns the minimum SDK version on which this signer should be verified.
+             */
+            public int getMinSdkVersion() {
+                return mMinSdkVersion;
+            }
+
+            /**
+             * Returns the maximum SDK version on which this signer should be verified.
+             */
+            public int getMaxSdkVersion() {
+                return mMaxSdkVersion;
+            }
+
+            /**
+             * Returns whether rotation is targeting a development release.
+             *
+             * <p>A development release uses the SDK version of the previously released platform
+             * until the SDK of the development release is finalized. To allow rotation to target
+             * a development release after T, this attribute must be set to ensure rotation is
+             * used on the development release but ignored on the released platform with the same
+             * API level.
+             */
+            public boolean getRotationTargetsDevRelease() {
+                return mRotationTargetsDevRelease;
+            }
+
+            /**
+             * Returns the {@link SigningCertificateLineage} for this signer; when an APK has
+             * SDK targeted signing configs, the lineage of each signer could potentially contain
+             * a subset of the full signing lineage and / or different capabilities for each signer
+             * in the lineage.
+             */
+            public SigningCertificateLineage getSigningCertificateLineage() {
+                return mSigningCertificateLineage;
+            }
+        }
+
+        /**
+         * Information about an APK Signature Scheme V4 signer associated with the APK's
+         * signature.
+         */
+        public static class V4SchemeSignerInfo {
+            private final int mIndex;
+            private final List<X509Certificate> mCerts;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+            private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+                    mContentDigests;
+
+            private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+                mIndex = result.index;
+                mCerts = result.certs;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+                mContentDigests = result.contentDigests;
+            }
+
+            /**
+             * Returns this signer's {@code 0}-based index in the list of signers contained in the
+             * APK's APK Signature Scheme v3 signature.
+             */
+            public int getIndex() {
+                return mIndex;
+            }
+
+            /**
+             * Returns this signer's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the signer's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCerts.isEmpty() ? null : mCerts.get(0);
+            }
+
+            /**
+             * Returns this signer's certificates. The first certificate is for the signer's public
+             * key. An empty list may be returned if an error was encountered during verification
+             * (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificates() {
+                return mCerts;
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+                return mContentDigests;
+            }
+        }
+
+        /**
+         * Information about SourceStamp associated with the APK's signature.
+         */
+        public static class SourceStampInfo {
+            public enum SourceStampVerificationStatus {
+                /** The stamp is present and was successfully verified. */
+                STAMP_VERIFIED,
+                /** The stamp is present but failed verification. */
+                STAMP_VERIFICATION_FAILED,
+                /** The expected cert digest did not match the digest in the APK. */
+                CERT_DIGEST_MISMATCH,
+                /** The stamp is not present at all. */
+                STAMP_MISSING,
+                /** The stamp is at least partially present, but was not able to be verified. */
+                STAMP_NOT_VERIFIED,
+                /** The stamp was not able to be verified due to an unexpected error. */
+                VERIFICATION_ERROR
+            }
+
+            private final List<X509Certificate> mCertificates;
+            private final List<X509Certificate> mCertificateLineage;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+            private final List<IssueWithParams> mInfoMessages;
+
+            private final SourceStampVerificationStatus mSourceStampVerificationStatus;
+
+            private final long mTimestamp;
+
+            private SourceStampInfo(ApkSignerInfo result) {
+                mCertificates = result.certs;
+                mCertificateLineage = result.certificateLineage;
+                mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+                        result.getErrors());
+                mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+                        result.getWarnings());
+                mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+                        result.getInfoMessages());
+                if (mErrors.isEmpty() && mWarnings.isEmpty()) {
+                    mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
+                } else {
+                    mSourceStampVerificationStatus =
+                            SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
+                }
+                mTimestamp = result.timestamp;
+            }
+
+            SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
+                mCertificates = Collections.emptyList();
+                mCertificateLineage = Collections.emptyList();
+                mErrors = Collections.emptyList();
+                mWarnings = Collections.emptyList();
+                mInfoMessages = Collections.emptyList();
+                mSourceStampVerificationStatus = sourceStampVerificationStatus;
+                mTimestamp = 0;
+            }
+
+            /**
+             * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the SourceStamp's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCertificates.isEmpty() ? null : mCertificates.get(0);
+            }
+
+            /**
+             * Returns a list containing all of the certificates in the stamp certificate lineage.
+             */
+            public List<X509Certificate> getCertificatesInLineage() {
+                return mCertificateLineage;
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            /**
+             * Returns {@code true} if any info messages were encountered during verification of
+             * this source stamp.
+             */
+            public boolean containsInfoMessages() {
+                return !mInfoMessages.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            /**
+             * Returns a {@code List} of {@link IssueWithParams} representing info messages
+             * that were encountered during verification of the source stamp.
+             */
+            public List<IssueWithParams> getInfoMessages() {
+                return mInfoMessages;
+            }
+
+            /**
+             * Returns the reason for any source stamp verification failures, or {@code
+             * STAMP_VERIFIED} if the source stamp was successfully verified.
+             */
+            public SourceStampVerificationStatus getSourceStampVerificationStatus() {
+                return mSourceStampVerificationStatus;
+            }
+
+            /**
+             * Returns the epoch timestamp in seconds representing the time this source stamp block
+             * was signed, or 0 if the timestamp is not available.
+             */
+            public long getTimestampEpochSeconds() {
+                return mTimestamp;
+            }
+        }
+    }
+
+    /**
+     * Error or warning encountered while verifying an APK's signatures.
+     */
+    public enum Issue {
+
+        /**
+         * APK is not JAR-signed.
+         */
+        JAR_SIG_NO_SIGNATURES("No JAR signatures"),
+
+        /**
+         * APK signature scheme v1 has exceeded the maximum number of jar signers.
+         * <ul>
+         * <li>Parameter 1: maximum allowed signers ({@code Integer})</li>
+         * <li>Parameter 2: total number of signers ({@code Integer})</li>
+         * </ul>
+         */
+        JAR_SIG_MAX_SIGNATURES_EXCEEDED(
+                "APK Signature Scheme v1 only supports a maximum of %1$d signers, found %2$d"),
+
+        /**
+         * APK does not contain any entries covered by JAR signatures.
+         */
+        JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"),
+
+        /**
+         * APK contains multiple entries with the same name.
+         *
+         * <ul>
+         * <li>Parameter 1: name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
+
+        /**
+         * JAR manifest contains a section with a duplicate name.
+         *
+         * <ul>
+         * <li>Parameter 1: section name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
+
+        /**
+         * JAR manifest contains a section without a name.
+         *
+         * <ul>
+         * <li>Parameter 1: section index (1-based) ({@code Integer})</li>
+         * </ul>
+         */
+        JAR_SIG_UNNNAMED_MANIFEST_SECTION(
+                "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"),
+
+        /**
+         * JAR signature file contains a section without a name.
+         *
+         * <ul>
+         * <li>Parameter 1: signature file name ({@code String})</li>
+         * <li>Parameter 2: section index (1-based) ({@code Integer})</li>
+         * </ul>
+         */
+        JAR_SIG_UNNNAMED_SIG_FILE_SECTION(
+                "Malformed %1$s: invidual section #%2$d does not have a name"),
+
+        /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */
+        JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"),
+
+        /**
+         * JAR manifest references an entry which is not there in the APK.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST(
+                "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"),
+
+        /**
+         * JAR manifest does not list a digest for the specified entry.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"),
+
+        /**
+         * JAR signature does not list a digest for the specified entry.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * <li>Parameter 2: signature file name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"),
+
+        /**
+         * The specified JAR entry is not covered by JAR signature.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"),
+
+        /**
+         * JAR signature uses different set of signers to protect the two specified ZIP entries.
+         *
+         * <ul>
+         * <li>Parameter 1: first entry name ({@code String})</li>
+         * <li>Parameter 2: first entry signer names ({@code List<String>})</li>
+         * <li>Parameter 3: second entry name ({@code String})</li>
+         * <li>Parameter 4: second entry signer names ({@code List<String>})</li>
+         * </ul>
+         */
+        JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH(
+                "Entries %1$s and %3$s are signed with different sets of signers"
+                        + " : <%2$s> vs <%4$s>"),
+
+        /**
+         * Digest of the specified ZIP entry's data does not match the digest expected by the JAR
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
+         * <li>Parameter 3: name of the entry in which the expected digest is specified
+         *     ({@code String})</li>
+         * <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
+         * <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY(
+                "%2$s digest of %1$s does not match the digest specified in %3$s"
+                        + ". Expected: <%5$s>, actual: <%4$s>"),
+
+        /**
+         * Digest of the JAR manifest main section did not verify.
+         *
+         * <ul>
+         * <li>Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})</li>
+         * <li>Parameter 2: name of the entry in which the expected digest is specified
+         *     ({@code String})</li>
+         * <li>Parameter 3: base64-encoded actual digest ({@code String})</li>
+         * <li>Parameter 4: base64-encoded expected digest ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY(
+                "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest"
+                        + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"),
+
+        /**
+         * Digest of the specified JAR manifest section does not match the digest expected by the
+         * JAR signature.
+         *
+         * <ul>
+         * <li>Parameter 1: section name ({@code String})</li>
+         * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
+         * <li>Parameter 3: name of the signature file in which the expected digest is specified
+         *     ({@code String})</li>
+         * <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
+         * <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY(
+                "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest"
+                        + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"),
+
+        /**
+         * JAR signature file does not contain the whole-file digest of the JAR manifest file. The
+         * digest speeds up verification of JAR signature.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE(
+                "%1$s does not specify digest of META-INF/MANIFEST.MF"
+                        + ". This slows down verification."),
+
+        /**
+         * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not
+         * contain protections against stripping of these newer scheme signatures.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_APK_SIG_STRIP_PROTECTION(
+                "APK is signed using APK Signature Scheme v2 but these signatures may be stripped"
+                        + " without being detected because %1$s does not contain anti-stripping"
+                        + " protections."),
+
+        /**
+         * JAR signature of the signer is missing a file/entry.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the encountered file ({@code String})</li>
+         * <li>Parameter 2: name of the missing file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"),
+
+        /**
+         * An exception was encountered while verifying JAR signature contained in a signature block
+         * against the signature file.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: name of the signature file ({@code String})</li>
+         * <li>Parameter 3: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
+
+        /**
+         * JAR signature contains unsupported digest algorithm.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: digest algorithm OID ({@code String})</li>
+         * <li>Parameter 3: signature algorithm OID ({@code String})</li>
+         * <li>Parameter 4: API Levels on which this combination of algorithms is not supported
+         *     ({@code String})</li>
+         * <li>Parameter 5: user-friendly variant of digest algorithm ({@code String})</li>
+         * <li>Parameter 6: user-friendly variant of signature algorithm ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_UNSUPPORTED_SIG_ALG(
+                "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which"
+                        + " is not supported on API Level(s) %4$s for which this APK is being"
+                        + " verified"),
+
+        /**
+         * An exception was encountered while parsing JAR signature contained in a signature block.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"),
+
+        /**
+         * An exception was encountered while parsing a certificate contained in the JAR signature
+         * block.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"),
+
+        /**
+         * JAR signature contained in a signature block file did not verify against the signature
+         * file.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: name of the signature file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
+
+        /**
+         * JAR signature contains no verified signers.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
+
+        /**
+         * JAR signature file contains a section with a duplicate name.
+         *
+         * <ul>
+         * <li>Parameter 1: signature file name ({@code String})</li>
+         * <li>Parameter 1: section name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"),
+
+        /**
+         * JAR signature file's main section doesn't contain the mandatory Signature-Version
+         * attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: signature file name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE(
+                "Malformed %1$s: missing Signature-Version attribute"),
+
+        /**
+         * JAR signature file references an unknown APK signature scheme ID.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
+         * </ul>
+         */
+        JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+                "JAR signature %1$s references unknown APK signature scheme ID: %2$d"),
+
+        /**
+         * JAR signature file indicates that the APK is supposed to be signed with a supported APK
+         * signature scheme (in addition to the JAR signature) but no such signature was found in
+         * the APK.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * <li>Parameter 2: APK signature scheme ID ({@code} Integer)</li>
+         * <li>Parameter 3: APK signature scheme English name ({@code} String)</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_APK_SIG_REFERENCED(
+                "JAR signature %1$s indicates the APK is signed using %3$s but no such signature"
+                        + " was found. Signature stripped?"),
+
+        /**
+         * JAR entry is not covered by signature and thus unauthorized modifications to its contents
+         * will not be detected.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_UNPROTECTED_ZIP_ENTRY(
+                "%1$s not protected by signature. Unauthorized modifications to this JAR entry"
+                        + " will not be detected. Delete or move the entry outside of META-INF/."),
+
+        /**
+         * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK
+         * Signature Scheme v2 signature from this signer, but does not contain a JAR signature
+         * from this signer.
+         */
+        JAR_SIG_MISSING("No JAR signature from this signer"),
+
+        /**
+         * APK is targeting a sandbox version which requires APK Signature Scheme v2 signature but
+         * no such signature was found.
+         *
+         * <ul>
+         * <li>Parameter 1: target sandbox version ({@code Integer})</li>
+         * </ul>
+         */
+        NO_SIG_FOR_TARGET_SANDBOX_VERSION(
+                "Missing APK Signature Scheme v2 signature required for target sandbox version"
+                        + " %1$d"),
+
+        /**
+         * APK is targeting an SDK version that requires a minimum signature scheme version, but the
+         * APK is not signed with that version or later.
+         *
+         * <ul>
+         *     <li>Parameter 1: target SDK Version (@code Integer})</li>
+         *     <li>Parameter 2: minimum signature scheme version ((@code Integer})</li>
+         * </ul>
+         */
+        MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET(
+                "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is"
+                        + " not signed with this or a later signature scheme"),
+
+        /**
+         * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
+         * signature from this signer, but does not contain an APK Signature Scheme v2 signature
+         * from this signer.
+         */
+        V2_SIG_MISSING("No APK Signature Scheme v2 signature from this signer"),
+
+        /**
+         * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
+         */
+        V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+        /**
+         * Failed to parse this signer's signer block contained in the APK Signature Scheme v2
+         * signature.
+         */
+        V2_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+        /**
+         * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be
+         * parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+        /**
+         * This APK Signature Scheme v2 signer's certificate could not be parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+         *     certificates ({@code Integer})</li>
+         * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+         *     list of certificates ({@code Integer})</li>
+         * <li>Parameter 3: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+        /**
+         * Failed to parse this signer's signature record contained in the APK Signature Scheme v2
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"),
+
+        /**
+         * Failed to parse this signer's digest record contained in the APK Signature Scheme v2
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
+
+        /**
+         * This APK Signature Scheme v2 signer contains a malformed additional attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+        /**
+         * APK Signature Scheme v2 signature references an unknown APK signature scheme ID.
+         *
+         * <ul>
+         * <li>Parameter 1: signer index ({@code Integer})</li>
+         * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
+         * </ul>
+         */
+        V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+                "APK Signature Scheme v2 signer: %1$s references unknown APK signature scheme ID: "
+                        + "%2$d"),
+
+        /**
+         * APK Signature Scheme v2 signature indicates that the APK is supposed to be signed with a
+         * supported APK signature scheme (in addition to the v2 signature) but no such signature
+         * was found in the APK.
+         *
+         * <ul>
+         * <li>Parameter 1: signer index ({@code Integer})</li>
+         * <li>Parameter 2: APK signature scheme English name ({@code} String)</li>
+         * </ul>
+         */
+        V2_SIG_MISSING_APK_SIG_REFERENCED(
+                "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but "
+                        + "no such signature was found. Signature stripped?"),
+
+        /**
+         * APK signature scheme v2 has exceeded the maximum number of signers.
+         * <ul>
+         * <li>Parameter 1: maximum allowed signers ({@code Integer})</li>
+         * <li>Parameter 2: total number of signers ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MAX_SIGNATURES_EXCEEDED(
+                "APK Signature Scheme V2 only supports a maximum of %1$d signers, found %2$d"),
+
+        /**
+         * APK Signature Scheme v2 signature contains no signers.
+         */
+        V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"),
+
+        /**
+         * This APK Signature Scheme v2 signer contains a signature produced using an unknown
+         * algorithm.
+         *
+         * <ul>
+         * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+        /**
+         * This APK Signature Scheme v2 signer contains an unknown additional attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute ID ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+        /**
+         * An exception was encountered while verifying APK Signature Scheme v2 signature of this
+         * signer.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * APK Signature Scheme v2 signature over this signer's signed-data block did not verify.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+         * </ul>
+         */
+        V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+        /**
+         * This APK Signature Scheme v2 signer offers no signatures.
+         */
+        V2_SIG_NO_SIGNATURES("No signatures"),
+
+        /**
+         * This APK Signature Scheme v2 signer offers signatures but none of them are supported.
+         */
+        V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures: %1$s"),
+
+        /**
+         * This APK Signature Scheme v2 signer offers no certificates.
+         */
+        V2_SIG_NO_CERTIFICATES("No certificates"),
+
+        /**
+         * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does
+         * not match the public key listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+         * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
+         * </ul>
+         */
+        V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+                "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+        /**
+         * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures
+         * record do not match the signature algorithms listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
+         * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
+         * </ul>
+         */
+        V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+                "Signature algorithms mismatch between signatures and digests records"
+                        + ": %1$s vs %2$s"),
+
+        /**
+         * The APK's digest does not match the digest contained in the APK Signature Scheme v2
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+         * </ul>
+         */
+        V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
+                "APK integrity check failed. %1$s digest mismatch."
+                        + " Expected: <%2$s>, actual: <%3$s>"),
+
+        /**
+         * Failed to parse the list of signers contained in the APK Signature Scheme v3 signature.
+         */
+        V3_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+        /**
+         * Failed to parse this signer's signer block contained in the APK Signature Scheme v3
+         * signature.
+         */
+        V3_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+        /**
+         * Public key embedded in the APK Signature Scheme v3 signature of this signer could not be
+         * parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+        /**
+         * This APK Signature Scheme v3 signer's certificate could not be parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+         *     certificates ({@code Integer})</li>
+         * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+         *     list of certificates ({@code Integer})</li>
+         * <li>Parameter 3: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V3_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+        /**
+         * Failed to parse this signer's signature record contained in the APK Signature Scheme v3
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V3_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v3 signature record #%1$d"),
+
+        /**
+         * Failed to parse this signer's digest record contained in the APK Signature Scheme v3
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"),
+
+        /**
+         * This APK Signature Scheme v3 signer contains a malformed additional attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+         * </ul>
+         */
+        V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+        /**
+         * APK Signature Scheme v3 signature contains no signers.
+         */
+        V3_SIG_NO_SIGNERS("No signers in APK Signature Scheme v3 signature"),
+
+        /**
+         * APK Signature Scheme v3 signature contains multiple signers (only one allowed per
+         * platform version).
+         */
+        V3_SIG_MULTIPLE_SIGNERS("Multiple APK Signature Scheme v3 signatures found for a single "
+                + " platform version."),
+
+        /**
+         * APK Signature Scheme v3 signature found, but multiple v1 and/or multiple v2 signers
+         * found, where only one may be used with APK Signature Scheme v3
+         */
+        V3_SIG_MULTIPLE_PAST_SIGNERS("Multiple signatures found for pre-v3 signing with an APK "
+                + " Signature Scheme v3 signer.  Only one allowed."),
+
+        /**
+         * APK Signature Scheme v3 signature found, but its signer doesn't match the v1/v2 signers,
+         * or have them as the root of its signing certificate history
+         */
+        V3_SIG_PAST_SIGNERS_MISMATCH(
+                "v3 signer differs from v1/v2 signer without proper signing certificate lineage."),
+
+        /**
+         * This APK Signature Scheme v3 signer contains a signature produced using an unknown
+         * algorithm.
+         *
+         * <ul>
+         * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+         * </ul>
+         */
+        V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+        /**
+         * This APK Signature Scheme v3 signer contains an unknown additional attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute ID ({@code Integer})</li>
+         * </ul>
+         */
+        V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+        /**
+         * 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>
+         */
+        V3_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * The APK Signature Scheme v3 signer contained an invalid value for either min or max SDK
+         * versions.
+         *
+         * <ul>
+         * <li>Parameter 1: minSdkVersion ({@code Integer})
+         * <li>Parameter 2: maxSdkVersion ({@code Integer})
+         * </ul>
+         */
+        V3_SIG_INVALID_SDK_VERSIONS("Invalid SDK Version parameter(s) encountered in APK Signature "
+                + "scheme v3 signature: minSdkVersion %1$s maxSdkVersion: %2$s"),
+
+        /**
+         * 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>
+         */
+        V3_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+        /**
+         * This APK Signature Scheme v3 signer offers no signatures.
+         */
+        V3_SIG_NO_SIGNATURES("No signatures"),
+
+        /**
+         * This APK Signature Scheme v3 signer offers signatures but none of them are supported.
+         */
+        V3_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+
+        /**
+         * This APK Signature Scheme v3 signer offers no certificates.
+         */
+        V3_SIG_NO_CERTIFICATES("No certificates"),
+
+        /**
+         * This APK Signature Scheme v3 signer's minSdkVersion listed in the signer's signed data
+         * does not match the minSdkVersion listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: minSdkVersion in signature record ({@code Integer}) </li>
+         * <li>Parameter 2: minSdkVersion in signed data ({@code Integer}) </li>
+         * </ul>
+         */
+        V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD(
+                "minSdkVersion mismatch between signed data and signature record:"
+                        + " <%1$s> vs <%2$s>"),
+
+        /**
+         * This APK Signature Scheme v3 signer's maxSdkVersion listed in the signer's signed data
+         * does not match the maxSdkVersion listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: maxSdkVersion in signature record ({@code Integer}) </li>
+         * <li>Parameter 2: maxSdkVersion in signed data ({@code Integer}) </li>
+         * </ul>
+         */
+        V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD(
+                "maxSdkVersion mismatch between signed data and signature record:"
+                        + " <%1$s> vs <%2$s>"),
+
+        /**
+         * This APK Signature Scheme v3 signer's public key listed in the signer's certificate does
+         * not match the public key listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+         * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
+         * </ul>
+         */
+        V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+                "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+        /**
+         * This APK Signature Scheme v3 signer's signature algorithms listed in the signatures
+         * record do not match the signature algorithms listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
+         * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
+         * </ul>
+         */
+        V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+                "Signature algorithms mismatch between signatures and digests records"
+                        + ": %1$s vs %2$s"),
+
+        /**
+         * The APK's digest does not match the digest contained in the APK Signature Scheme v3
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+         * </ul>
+         */
+        V3_SIG_APK_DIGEST_DID_NOT_VERIFY(
+                "APK integrity check failed. %1$s digest mismatch."
+                        + " Expected: <%2$s>, actual: <%3$s>"),
+
+        /**
+         * The signer's SigningCertificateLineage attribute containd a proof-of-rotation record with
+         * signature(s) that did not verify.
+         */
+        V3_SIG_POR_DID_NOT_VERIFY("SigningCertificateLineage attribute containd a proof-of-rotation"
+                + " record with signature(s) that did not verify."),
+
+        /**
+         * Failed to parse the SigningCertificateLineage structure in the APK Signature Scheme v3
+         * signature's additional attributes section.
+         */
+        V3_SIG_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage structure in the "
+                + "APK Signature Scheme v3 signature's additional attributes section."),
+
+        /**
+         * The APK's signing certificate does not match the terminal node in the provided
+         * proof-of-rotation structure describing the signing certificate history
+         */
+        V3_SIG_POR_CERT_MISMATCH(
+                "APK signing certificate differs from the associated certificate found in the "
+                        + "signer's SigningCertificateLineage."),
+
+        /**
+         * The APK Signature Scheme v3 signers encountered do not offer a continuous set of
+         * supported platform versions.  Either they overlap, resulting in potentially two
+         * acceptable signers for a platform version, or there are holes which would create problems
+         * in the event of platform version upgrades.
+         */
+        V3_INCONSISTENT_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK "
+                + "versions are not continuous."),
+
+        /**
+         * The APK Signature Scheme v3 signers don't cover all requested SDK versions.
+         *
+         *  <ul>
+         * <li>Parameter 1: minSdkVersion ({@code Integer})
+         * <li>Parameter 2: maxSdkVersion ({@code Integer})
+         * </ul>
+         */
+        V3_MISSING_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK "
+                + "versions do not cover the entire desired range.  Found min:  %1$s max %2$s"),
+
+        /**
+         * The SigningCertificateLineages for different platform versions using APK Signature Scheme
+         * v3 do not go together.  Specifically, each should be a subset of another, with the size
+         * of each increasing as the platform level increases.
+         */
+        V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions"
+                + " using APK Signature Scheme v3 are not all a part of the same overall lineage."),
+
+        /**
+         * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block
+         * was not found.
+         *
+         * <ul>
+         * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+         * </ul>
+         */
+        V31_BLOCK_MISSING(
+                "The v3 signer indicates key rotation should be supported starting from SDK "
+                        + "version %1$s, but a v3.1 block was not found"),
+
+        /**
+         * The v3 stripping protection attribute for rotation does not match the minimum SDK version
+         * targeting rotation in the v3.1 signer block.
+         *
+         * <ul>
+         * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+         * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer})
+         * </ul>
+         */
+        V31_ROTATION_MIN_SDK_MISMATCH(
+                "The v3 signer indicates key rotation should be supported starting from SDK "
+                        + "version %1$s, but the v3.1 block targets %2$s for rotation"),
+
+        /**
+         * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min
+         * SDK version stripping protection attribute was not written to the v3 signer.
+         *
+         * <ul>
+         * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer})
+         * </ul>
+         */
+        V31_ROTATION_MIN_SDK_ATTR_MISSING(
+                "APK supports key rotation starting from SDK version %1$s, but the v3 signer does"
+                        + " not contain the attribute to detect if this signature is stripped"),
+
+        /**
+         * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only
+         * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the
+         * same as the SDK version for rotation then this should be written to a v3.0 block.
+         */
+        V31_BLOCK_FOUND_WITHOUT_V3_BLOCK(
+                "The APK contains a v3.1 signing block without a v3.0 base block"),
+
+        /**
+         * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in
+         * the signer; this attribute is only intended for v3.1 signers to indicate they should be
+         * targeting the next development release that is using the SDK version of the previously
+         * released platform SDK version.
+         */
+        V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER(
+                "The rotation-targets-dev-release attribute is only supported on v3.1 signers; "
+                        + "this attribute will be ignored by the platform in a v3.0 signer"),
+
+        /**
+         * APK Signing Block contains an unknown entry.
+         *
+         * <ul>
+         * <li>Parameter 1: entry ID ({@code Integer})</li>
+         * </ul>
+         */
+        APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"),
+
+        /**
+         * Failed to parse this signer's signature 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_SIGNERS(
+                "V4 signature has malformed signer block"),
+
+        /**
+         * This APK Signature Scheme V4 signer contains a signature produced using an
+         * unknown algorithm.
+         *
+         * <ul>
+         * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+         * </ul>
+         */
+        V4_SIG_UNKNOWN_SIG_ALGORITHM(
+                "V4 signature has unknown signing algorithm: %1$#x"),
+
+        /**
+         * This APK Signature Scheme V4 signer offers no signatures.
+         */
+        V4_SIG_NO_SIGNATURES(
+                "V4 signature has no signature found"),
+
+        /**
+         * This APK Signature Scheme V4 signer offers signatures but none of them are
+         * supported.
+         */
+        V4_SIG_NO_SUPPORTED_SIGNATURES(
+                "V4 signature has no supported signature"),
+
+        /**
+         * 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("%1$s signature over signed-data did not verify"),
+
+        /**
+         * 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("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.
+         *
+         * <ul>
+         * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+         *     certificates ({@code Integer})</li>
+         * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+         *     list of certificates ({@code Integer})</li>
+         * <li>Parameter 3: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V4_SIG_MALFORMED_CERTIFICATE(
+                "V4 signature has malformed certificate"),
+
+        /**
+         * This APK Signature Scheme V4 signer offers no certificate.
+         */
+        V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"),
+
+        /**
+         * This APK Signature Scheme V4 signer's public key listed in the signer's
+         * certificate does not match the public key listed in the signature proto.
+         *
+         * <ul>
+         * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+         * <li>Parameter 2: hex-encoded public key from signature proto ({@code String})</li>
+         * </ul>
+         */
+        V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+                "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
+         * Scheme V4 signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+         * </ul>
+         */
+        V4_SIG_APK_ROOT_DID_NOT_VERIFY(
+                "V4 signature's hash tree root (content digest) did not verity"),
+
+        /**
+         * The APK's hash tree does not match the hash tree contained in the Signature
+         * Scheme V4 signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected hash tree of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual hash tree of the APK ({@code String})</li>
+         * </ul>
+         */
+        V4_SIG_APK_TREE_DID_NOT_VERIFY(
+                "V4 signature's hash tree did not verity"),
+
+        /**
+         * Using more than one Signer to sign APK Signature Scheme V4 signature.
+         */
+        V4_SIG_MULTIPLE_SIGNERS(
+                "V4 signature only supports one signer"),
+
+        /**
+         * V4.1 signature requires two signers to match the v3 and the v3.1.
+         */
+        V41_SIG_NEEDS_TWO_SIGNERS("V4.1 signature requires two signers"),
+
+        /**
+         * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer
+         * used to sign APK Signature Scheme V4 signature.
+         */
+        V4_SIG_V2_V3_SIGNERS_MISMATCH(
+                "V4 signature and V2/V3 signature have mismatched certificates"),
+
+        /**
+         * The v4 signature's digest does not match the digest from the corresponding v2 / v3
+         * signature.
+         *
+         * <ul>
+         *     <li>Parameter 1: Signature scheme of mismatched digest ({@code int})
+         *     <li>Parameter 2: v2/v3 digest ({@code String})
+         *     <li>Parameter 3: v4 digest ({@code String})
+         * </ul>
+         */
+        V4_SIG_V2_V3_DIGESTS_MISMATCH(
+                "V4 signature and V%1$d signature have mismatched digests, V%1$d digest: %2$s, V4"
+                        + " digest: %3$s"),
+
+        /**
+         * The v4 signature does not contain the expected number of digests.
+         *
+         * <ul>
+         *     <li>Parameter 1: Number of digests found ({@code int})
+         * </ul>
+         */
+        V4_SIG_UNEXPECTED_DIGESTS(
+                "V4 signature does not have the expected number of digests, found %1$d"),
+
+        /**
+         * The v4 signature format version isn't the same as the tool's current version, something
+         * may go wrong.
+         */
+        V4_SIG_VERSION_NOT_CURRENT(
+                "V4 signature format version %1$d is different from the tool's current "
+                        + "version %2$d"),
+
+        /**
+         * The APK does not contain the source stamp certificate digest file nor the signature block
+         * when verification expected a source stamp to be present.
+         */
+        SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING(
+                "Neither the source stamp certificate digest file nor the signature block are "
+                        + "present in the APK"),
+
+        /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
+        SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
+
+        /**
+         * SourceStamp's certificate could not be parsed.
+         *
+         * <ul>
+         *   <li>Parameter 1: error details ({@code Throwable})
+         * </ul>
+         */
+        SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"),
+
+        /** Failed to parse SourceStamp's signature. */
+        SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"),
+
+        /**
+         * SourceStamp contains a signature produced using an unknown algorithm.
+         *
+         * <ul>
+         *   <li>Parameter 1: algorithm ID ({@code Integer})
+         * </ul>
+         */
+        SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+        /**
+         * An exception was encountered while verifying SourceStamp signature.
+         *
+         * <ul>
+         *   <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+         *   <li>Parameter 2: exception ({@code Throwable})
+         * </ul>
+         */
+        SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * SourceStamp signature block did not verify.
+         *
+         * <ul>
+         *   <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+         * </ul>
+         */
+        SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+        /** SourceStamp offers no signatures. */
+        SOURCE_STAMP_NO_SIGNATURE("No signature"),
+
+        /**
+         * SourceStamp offers an unsupported signature.
+         * <ul>
+         *     <li>Parameter 1: list of {@link SignatureAlgorithm}s  in the source stamp
+         *     signing block.
+         *     <li>Parameter 2: {@code Exception} caught when attempting to obtain the list of
+         *     supported signatures.
+         * </ul>
+         */
+        SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"),
+
+        /**
+         * SourceStamp's certificate listed in the APK signing block does not match the certificate
+         * listed in the SourceStamp file in the APK.
+         *
+         * <ul>
+         *   <li>Parameter 1: SHA-256 hash of certificate from SourceStamp block in APK signing
+         *       block ({@code String})
+         *   <li>Parameter 2: SHA-256 hash of certificate from SourceStamp file in APK ({@code
+         *       String})
+         * </ul>
+         */
+        SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
+                "Certificate mismatch between SourceStamp block in APK signing block and"
+                        + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
+
+        /**
+         * The APK contains a source stamp signature block without the expected certificate digest
+         * in the APK contents.
+         */
+        SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST(
+                "A source stamp signature block was found without a corresponding certificate "
+                        + "digest in the APK"),
+
+        /**
+         * When verifying just the source stamp, the certificate digest in the APK does not match
+         * the expected digest.
+         * <ul>
+         *     <li>Parameter 1: SHA-256 digest of the source stamp certificate in the APK.
+         *     <li>Parameter 2: SHA-256 digest of the expected source stamp certificate.
+         * </ul>
+         */
+        SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH(
+                "The source stamp certificate digest in the APK, %1$s, does not match the "
+                        + "expected digest, %2$s"),
+
+        /**
+         * Source stamp block contains a malformed attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+         * </ul>
+         */
+        SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"),
+
+        /**
+         * Source stamp block contains an unknown attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute ID ({@code Integer})</li>
+         * </ul>
+         */
+        SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"),
+
+        /**
+         * Failed to parse the SigningCertificateLineage structure in the source stamp
+         * attributes section.
+         */
+        SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage "
+                + "structure in the source stamp attributes section."),
+
+        /**
+         * The source stamp certificate does not match the terminal node in the provided
+         * proof-of-rotation structure describing the stamp certificate history.
+         */
+        SOURCE_STAMP_POR_CERT_MISMATCH(
+                "APK signing certificate differs from the associated certificate found in the "
+                        + "signer's SigningCertificateLineage."),
+
+        /**
+         * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+         * with signature(s) that did not verify.
+         */
+        SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute "
+                + "contains a proof-of-rotation record with signature(s) that did not verify."),
+
+        /**
+         * The source stamp timestamp attribute has an invalid value (<= 0).
+         * <ul>
+         *     <li>Parameter 1: The invalid timestamp value.
+         * </ul>
+         */
+        SOURCE_STAMP_INVALID_TIMESTAMP(
+                "The source stamp"
+                        + " timestamp attribute has an invalid value: %1$d"),
+
+        /**
+         * The APK could not be properly parsed due to a ZIP or APK format exception.
+         * <ul>
+         *     <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK.
+         * </ul>
+         */
+        MALFORMED_APK(
+                "Malformed APK; the following exception was caught when attempting to parse the "
+                        + "APK: %1$s"),
+
+        /**
+         * An unexpected exception was caught when attempting to verify the signature(s) within the
+         * APK.
+         * <ul>
+         *     <li>Parameter 1: The {@code Exception} caught during verification.
+         * </ul>
+         */
+        UNEXPECTED_EXCEPTION(
+                "An unexpected exception was caught when verifying the signature: %1$s");
+
+        private final String mFormat;
+
+        Issue(String format) {
+            mFormat = format;
+        }
+
+        /**
+         * Returns the format string suitable for combining the parameters of this issue into a
+         * readable string. See {@link java.util.Formatter} for format.
+         */
+        private String getFormat() {
+            return mFormat;
+        }
+    }
+
+    /**
+     * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
+     * form.
+     */
+    public static class IssueWithParams extends ApkVerificationIssue {
+        private final Issue mIssue;
+        private final Object[] mParams;
+
+        /**
+         * Constructs a new {@code IssueWithParams} of the specified type and with provided
+         * parameters.
+         */
+        public IssueWithParams(Issue issue, Object[] params) {
+            super(issue.mFormat, params);
+            mIssue = issue;
+            mParams = params;
+        }
+
+        /**
+         * Returns the type of this issue.
+         */
+        public Issue getIssue() {
+            return mIssue;
+        }
+
+        /**
+         * Returns the parameters of this issue.
+         */
+        public Object[] getParams() {
+            return mParams.clone();
+        }
+
+        /**
+         * Returns a readable form of this issue.
+         */
+        @Override
+        public String toString() {
+            return String.format(mIssue.getFormat(), mParams);
+        }
+    }
+
+    /**
+     * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate
+     * on the contents of the arrays rather than on references.
+     */
+    private static class ByteArray {
+        private final byte[] mArray;
+        private final int mHashCode;
+
+        private ByteArray(byte[] arr) {
+            mArray = arr;
+            mHashCode = Arrays.hashCode(mArray);
+        }
+
+        @Override
+        public int hashCode() {
+            return mHashCode;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof ByteArray)) {
+                return false;
+            }
+            ByteArray other = (ByteArray) obj;
+            if (hashCode() != other.hashCode()) {
+                return false;
+            }
+            if (!Arrays.equals(mArray, other.mArray)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Builder of {@link ApkVerifier} instances.
+     *
+     * <p>The resulting verifier by default checks whether the APK will verify on all platform
+     * versions supported by the APK, as specified by {@code android:minSdkVersion} attributes in
+     * the APK's {@code AndroidManifest.xml}. The range of platform versions can be customized using
+     * {@link #setMinCheckedPlatformVersion(int)} and {@link #setMaxCheckedPlatformVersion(int)}.
+     */
+    public static class Builder {
+        private final File mApkFile;
+        private final DataSource mApkDataSource;
+        private File mV4SignatureFile;
+
+        private Integer mMinSdkVersion;
+        private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+        /**
+         * Constructs a new {@code Builder} for verifying the provided APK file.
+         */
+        public Builder(File apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkFile = apk;
+            mApkDataSource = null;
+        }
+
+        /**
+         * Constructs a new {@code Builder} for verifying the provided APK.
+         */
+        public Builder(DataSource apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkDataSource = apk;
+            mApkFile = null;
+        }
+
+        /**
+         * Sets the oldest Android platform version for which the APK is verified. APK verification
+         * will confirm that the APK is expected to install successfully on all known Android
+         * platforms starting from the platform version with the provided API Level. The upper end
+         * of the platform versions range can be modified via
+         * {@link #setMaxCheckedPlatformVersion(int)}.
+         *
+         * <p>This method is useful for overriding the default behavior which checks that the APK
+         * will verify on all platform versions supported by the APK, as specified by
+         * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}.
+         *
+         * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+         * @see #setMinCheckedPlatformVersion(int)
+         */
+        public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+            mMinSdkVersion = minSdkVersion;
+            return this;
+        }
+
+        /**
+         * Sets the newest Android platform version for which the APK is verified. APK verification
+         * will confirm that the APK is expected to install successfully on all platform versions
+         * supported by the APK up until and including the provided version. The lower end
+         * of the platform versions range can be modified via
+         * {@link #setMinCheckedPlatformVersion(int)}.
+         *
+         * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+         * @see #setMinCheckedPlatformVersion(int)
+         */
+        public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+            mMaxSdkVersion = maxSdkVersion;
+            return this;
+        }
+
+        public Builder setV4SignatureFile(File v4SignatureFile) {
+            mV4SignatureFile = v4SignatureFile;
+            return this;
+        }
+
+        /**
+         * Returns an {@link ApkVerifier} initialized according to the configuration of this
+         * builder.
+         */
+        public ApkVerifier build() {
+            return new ApkVerifier(
+                    mApkFile,
+                    mApkDataSource,
+                    mV4SignatureFile,
+                    mMinSdkVersion,
+                    mMaxSdkVersion);
+        }
+    }
+
+    /**
+     * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link
+     * IssueWithParams} equivalent.
+     */
+    public static class ApkVerificationIssueAdapter {
+        private ApkVerificationIssueAdapter() {
+        }
+
+        // This field is visible for testing
+        static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>();
+
+        static {
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS,
+                    Issue.V2_SIG_MALFORMED_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS,
+                    Issue.V2_SIG_NO_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER,
+                    Issue.V2_SIG_MALFORMED_SIGNER);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE,
+                    Issue.V2_SIG_MALFORMED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES,
+                    Issue.V2_SIG_NO_SIGNATURES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE,
+                    Issue.V2_SIG_MALFORMED_CERTIFICATE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES,
+                    Issue.V2_SIG_NO_CERTIFICATES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST,
+                    Issue.V2_SIG_MALFORMED_DIGEST);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS,
+                    Issue.V3_SIG_MALFORMED_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS,
+                    Issue.V3_SIG_NO_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER,
+                    Issue.V3_SIG_MALFORMED_SIGNER);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE,
+                    Issue.V3_SIG_MALFORMED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES,
+                    Issue.V3_SIG_NO_SIGNATURES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE,
+                    Issue.V3_SIG_MALFORMED_CERTIFICATE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES,
+                    Issue.V3_SIG_NO_CERTIFICATES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST,
+                    Issue.V3_SIG_MALFORMED_DIGEST);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE,
+                    Issue.SOURCE_STAMP_NO_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE,
+                    Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+                    Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE,
+                    Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY,
+                    Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION,
+                    Issue.SOURCE_STAMP_VERIFY_EXCEPTION);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+                    Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST,
+                    Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING,
+                    Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+                    Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue
+                            .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+                    Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK,
+                    Issue.MALFORMED_APK);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION,
+                    Issue.UNEXPECTED_EXCEPTION);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING,
+                    Issue.SOURCE_STAMP_SIG_MISSING);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+                    Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE,
+                    Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE,
+                    Issue.SOURCE_STAMP_MALFORMED_LINEAGE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH,
+                    Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY,
+                    Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES,
+                    Issue.JAR_SIG_NO_SIGNATURES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+                    Issue.JAR_SIG_PARSE_EXCEPTION);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+                    Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+        }
+
+        /**
+         * Converts the provided {@code verificationIssues} to a {@code List} of corresponding
+         * {@link IssueWithParams} instances.
+         */
+        public static List<IssueWithParams> getIssuesFromVerificationIssues(
+                List<? extends ApkVerificationIssue> verificationIssues) {
+            List<IssueWithParams> result = new ArrayList<>(verificationIssues.size());
+            for (ApkVerificationIssue issue : verificationIssues) {
+                if (issue instanceof IssueWithParams) {
+                    result.add((IssueWithParams) issue);
+                } else {
+                    result.add(
+                            new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()),
+                                    issue.getParams()));
+                }
+            }
+            return result;
+        }
+    }
+}

+ 65 - 0
platform/android/java/editor/src/main/java/com/android/apksig/Constants.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+
+/**
+ * Exports internally defined constants to allow clients to reference these values without relying
+ * on internal code.
+ */
+public class Constants {
+    private Constants() {}
+
+    public static final int VERSION_SOURCE_STAMP = 0;
+    public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+    /**
+     * The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
+     */
+    public static final int MAX_APK_SIGNERS = 10;
+
+    /**
+     * The default page alignment for native library files in bytes.
+     */
+    public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384;
+
+    public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+            V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+    public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
+            V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+    public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+    public static final int V1_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+    public static final int V2_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+    public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
+}

+ 2241 - 0
platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java

@@ -0,0 +1,2241 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+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.apk.stamp.V2SourceStampSigner;
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeSigner;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4Signature;
+import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.TeeDataSink;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Default implementation of {@link ApkSignerEngine}.
+ *
+ * <p>Use {@link Builder} to obtain instances of this engine.
+ */
+public class DefaultApkSignerEngine implements ApkSignerEngine {
+
+    // IMPLEMENTATION NOTE: This engine generates a signed APK as follows:
+    // 1. The engine asks its client to output input JAR entries which are not part of JAR
+    //    signature.
+    // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to
+    //    compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects
+    //    the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the
+    //    file. It does not care about individual (i.e., JAR entry-specific) sections. It then
+    //    emits the v1 signature (a set of JAR entries) and asks the client to output them.
+    // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block
+    //    from outputZipSections() and asks its client to insert this block into the output.
+    // 4. If APK Signature Scheme v3 (v3 signing) is enabled, the engine includes it in the APK
+    //    Signing BLock output from outputZipSections() and asks its client to insert this block
+    //    into the output.  If both v2 and v3 signing is enabled, they are both added to the APK
+    //    Signing Block before asking the client to insert it into the output.
+
+    private final boolean mV1SigningEnabled;
+    private final boolean mV2SigningEnabled;
+    private final boolean mV3SigningEnabled;
+    private final boolean mVerityEnabled;
+    private final boolean mDebuggableApkPermitted;
+    private final boolean mOtherSignersSignaturesPreserved;
+    private final String mCreatedBy;
+    private final List<SignerConfig> mSignerConfigs;
+    private final List<SignerConfig> mTargetedSignerConfigs;
+    private final SignerConfig mSourceStampSignerConfig;
+    private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+    private final boolean mSourceStampTimestampEnabled;
+    private final int mMinSdkVersion;
+    private final SigningCertificateLineage mSigningCertificateLineage;
+
+    private List<byte[]> mPreservedV2Signers = Collections.emptyList();
+    private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList();
+
+    private List<V1SchemeSigner.SignerConfig> mV1SignerConfigs = Collections.emptyList();
+    private DigestAlgorithm mV1ContentDigestAlgorithm;
+
+    private boolean mClosed;
+
+    private boolean mV1SignaturePending;
+
+    /** Names of JAR entries which this engine is expected to output as part of v1 signing. */
+    private Set<String> mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
+
+    /** Requests for digests of output JAR entries. */
+    private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests =
+            new HashMap<>();
+
+    /** Digests of output JAR entries. */
+    private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>();
+
+    /** Data of JAR entries emitted by this engine as v1 signature. */
+    private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>();
+
+    /** Requests for data of output JAR entries which comprise the v1 signature. */
+    private final Map<String, GetJarEntryDataRequest> mOutputSignatureJarEntryDataRequests =
+            new HashMap<>();
+    /**
+     * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued.
+     */
+    private GetJarEntryDataRequest mInputJarManifestEntryDataRequest;
+
+    /**
+     * Request to obtain the data of AndroidManifest.xml or {@code null} if the request hasn't been
+     * issued.
+     */
+    private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest;
+
+    /**
+     * Whether the package being signed is marked as {@code android:debuggable} or {@code null} if
+     * this is not yet known.
+     */
+    private Boolean mDebuggable;
+
+    /**
+     * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued.
+     */
+    private OutputJarSignatureRequestImpl mAddV1SignatureRequest;
+
+    private boolean mV2SignaturePending;
+    private boolean mV3SignaturePending;
+
+    /**
+     * Request to output the emitted v2 and/or v3 signature(s) {@code null} if the request hasn't
+     * been issued.
+     */
+    private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest;
+
+    private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+
+    /**
+     * A Set of block IDs to be discarded when requesting to preserve the original signatures.
+     */
+    private static final Set<Integer> DISCARDED_SIGNATURE_BLOCK_IDS;
+    static {
+        DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3);
+        // The verity padding block is recomputed on an
+        // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary.
+        DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID);
+        // The source stamp block is not currently preserved; appending a new signature scheme
+        // block will invalidate the previous source stamp.
+        DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID);
+        DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID);
+    }
+
+    private DefaultApkSignerEngine(
+            List<SignerConfig> signerConfigs,
+            List<SignerConfig> targetedSignerConfigs,
+            SignerConfig sourceStampSignerConfig,
+            SigningCertificateLineage sourceStampSigningCertificateLineage,
+            boolean sourceStampTimestampEnabled,
+            int minSdkVersion,
+            boolean v1SigningEnabled,
+            boolean v2SigningEnabled,
+            boolean v3SigningEnabled,
+            boolean verityEnabled,
+            boolean debuggableApkPermitted,
+            boolean otherSignersSignaturesPreserved,
+            String createdBy,
+            SigningCertificateLineage signingCertificateLineage)
+            throws InvalidKeyException {
+        if (signerConfigs.isEmpty() && targetedSignerConfigs.isEmpty()) {
+            throw new IllegalArgumentException("At least one signer config must be provided");
+        }
+
+        mV1SigningEnabled = v1SigningEnabled;
+        mV2SigningEnabled = v2SigningEnabled;
+        mV3SigningEnabled = v3SigningEnabled;
+        mVerityEnabled = verityEnabled;
+        mV1SignaturePending = v1SigningEnabled;
+        mV2SignaturePending = v2SigningEnabled;
+        mV3SignaturePending = v3SigningEnabled;
+        mDebuggableApkPermitted = debuggableApkPermitted;
+        mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+        mCreatedBy = createdBy;
+        mSignerConfigs = signerConfigs;
+        mTargetedSignerConfigs = targetedSignerConfigs;
+        mSourceStampSignerConfig = sourceStampSignerConfig;
+        mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+        mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
+        mMinSdkVersion = minSdkVersion;
+        mSigningCertificateLineage = signingCertificateLineage;
+
+        if (v1SigningEnabled) {
+            if (v3SigningEnabled) {
+
+                // v3 signing only supports single signers, of which the oldest (first) will be the
+                // one to use for v1 and v2 signing
+                SignerConfig oldestConfig = !signerConfigs.isEmpty() ? signerConfigs.get(0)
+                        : targetedSignerConfigs.get(0);
+
+                // in the event of signing certificate changes, make sure we have the oldest in the
+                // signing history to sign with v1
+                if (signingCertificateLineage != null) {
+                    SigningCertificateLineage subLineage =
+                            signingCertificateLineage.getSubLineage(
+                                    oldestConfig.mCertificates.get(0));
+                    if (subLineage.size() != 1) {
+                        throw new IllegalArgumentException(
+                                "v1 signing enabled but the oldest signer in the"
+                                    + " SigningCertificateLineage is missing.  Please provide the"
+                                    + " oldest signer to enable v1 signing");
+                    }
+                }
+                createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion);
+            } else {
+                createV1SignerConfigs(signerConfigs, minSdkVersion);
+            }
+        }
+    }
+
+    private void createV1SignerConfigs(List<SignerConfig> signerConfigs, int minSdkVersion)
+            throws InvalidKeyException {
+        mV1SignerConfigs = new ArrayList<>(signerConfigs.size());
+        Map<String, Integer> v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size());
+        DigestAlgorithm v1ContentDigestAlgorithm = null;
+        for (int i = 0; i < signerConfigs.size(); i++) {
+            SignerConfig signerConfig = signerConfigs.get(i);
+            List<X509Certificate> certificates = signerConfig.getCertificates();
+            PublicKey publicKey = certificates.get(0).getPublicKey();
+
+            String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName());
+            // Check whether the signer's name is unique among all v1 signers
+            Integer indexOfOtherSignerWithSameName = v1SignerNameToSignerIndex.put(v1SignerName, i);
+            if (indexOfOtherSignerWithSameName != null) {
+                throw new IllegalArgumentException(
+                        "Signers #"
+                                + (indexOfOtherSignerWithSameName + 1)
+                                + " and #"
+                                + (i + 1)
+                                + " have the same name: "
+                                + v1SignerName
+                                + ". v1 signer names must be unique");
+            }
+
+            DigestAlgorithm v1SignatureDigestAlgorithm =
+                    V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(publicKey, minSdkVersion);
+            V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
+            v1SignerConfig.name = v1SignerName;
+            v1SignerConfig.privateKey = signerConfig.getPrivateKey();
+            v1SignerConfig.certificates = certificates;
+            v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
+            v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning();
+            // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm
+            // of comparable strength to the digest algorithm used for computing the signature.
+            // When there are multiple signers, pick the strongest digest algorithm out of their
+            // signature digest algorithms. This avoids reducing the digest strength used by any
+            // of the signers to protect APK contents.
+            if (v1ContentDigestAlgorithm == null) {
+                v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
+            } else {
+                if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
+                                v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
+                        > 0) {
+                    v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
+                }
+            }
+            mV1SignerConfigs.add(v1SignerConfig);
+        }
+        mV1ContentDigestAlgorithm = v1ContentDigestAlgorithm;
+        mSignatureExpectedOutputJarEntryNames =
+                V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs);
+    }
+
+    private List<ApkSigningBlockUtils.SignerConfig> createV2SignerConfigs(
+            boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+        if (mV3SigningEnabled) {
+
+            // v3 signing only supports single signers, of which the oldest (first) will be the one
+            // to use for v1 and v2 signing
+            List<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>();
+
+            SignerConfig oldestConfig = !mSignerConfigs.isEmpty() ? mSignerConfigs.get(0)
+                    : mTargetedSignerConfigs.get(0);
+
+            // first make sure that if we have signing certificate history that the oldest signer
+            // corresponds to the oldest ancestor
+            if (mSigningCertificateLineage != null) {
+                SigningCertificateLineage subLineage =
+                        mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0));
+                if (subLineage.size() != 1) {
+                    throw new IllegalArgumentException(
+                            "v2 signing enabled but the oldest signer in"
+                                    + " the SigningCertificateLineage is missing.  Please provide"
+                                    + " the oldest signer to enable v2 signing.");
+                }
+            }
+            signerConfig.add(
+                    createSigningBlockSignerConfig(
+                            oldestConfig,
+                            apkSigningBlockPaddingSupported,
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+            return signerConfig;
+        } else {
+            return createSigningBlockSignerConfigs(
+                    apkSigningBlockPaddingSupported,
+                    ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+        }
+    }
+
+    private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
+            List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
+        // If the caller only specified targeted signing configs, ensure those configs cover the
+        // full range for V3 support (or the APK's minSdkVersion if > P).
+        int minRequiredV3SdkVersion = Math.max(AndroidSdkVersion.P, mMinSdkVersion);
+        if (mSignerConfigs.isEmpty() &&
+                mTargetedSignerConfigs.get(0).getMinSdkVersion() > minRequiredV3SdkVersion) {
+            throw new IllegalArgumentException(
+                    "The provided targeted signer configs do not cover the SDK range for V3 "
+                            + "support; either provide the original signer or ensure a signer "
+                            + "targets SDK version " + minRequiredV3SdkVersion);
+        }
+
+        List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
+
+        // we have our configs, now touch them up to appropriately cover all SDK levels since APK
+        // signature scheme v3 was introduced
+        int currentMinSdk = Integer.MAX_VALUE;
+        for (int i = rawConfigs.size() - 1; i >= 0; i--) {
+            ApkSigningBlockUtils.SignerConfig config = rawConfigs.get(i);
+            if (config.signatureAlgorithms == null) {
+                // no valid algorithm was found for this signer, and we haven't yet covered all
+                // platform versions, something's wrong
+                String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm();
+                throw new InvalidKeyException(
+                        "Unsupported key algorithm "
+                                + keyAlgorithm
+                                + " is "
+                                + "not supported for APK Signature Scheme v3 signing");
+            }
+            if (i == rawConfigs.size() - 1) {
+                // first go through the loop, config should support all future platform versions.
+                // this assumes we don't deprecate support for signers in the future.  If we do,
+                // this needs to change
+                config.maxSdkVersion = Integer.MAX_VALUE;
+            } else {
+                // If the previous signer was targeting a development release, then the current
+                // signer's maxSdkVersion should overlap with the previous signer's minSdkVersion
+                // to ensure the current signer applies to the production release.
+                ApkSigningBlockUtils.SignerConfig prevSigner = processedConfigs.get(
+                        processedConfigs.size() - 1);
+                if (prevSigner.signerTargetsDevRelease) {
+                    config.maxSdkVersion = prevSigner.minSdkVersion;
+                } else {
+                    config.maxSdkVersion = currentMinSdk - 1;
+                }
+            }
+            if (config.minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+                // If the current signer is targeting the current development release, then set
+                // the signer's minSdkVersion to the last production release and the flag indicating
+                // this signer is targeting a dev release.
+                config.minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+                config.signerTargetsDevRelease = true;
+            } else if (config.minSdkVersion == 0) {
+                config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(
+                        config.signatureAlgorithms);
+            }
+            // Truncate the lineage to the current signer if it is not the latest signer.
+            X509Certificate signerCert = config.certificates.get(0);
+            if (config.signingCertificateLineage != null
+                    && !config.signingCertificateLineage.isCertificateLatestInLineage(signerCert)) {
+                config.signingCertificateLineage = config.signingCertificateLineage.getSubLineage(
+                        signerCert);
+            }
+            // we know that this config will be used, so add it to our result, order doesn't matter
+            // at this point
+            processedConfigs.add(config);
+            currentMinSdk = config.minSdkVersion;
+            if (config.signerTargetsDevRelease ? currentMinSdk < minRequiredV3SdkVersion
+                    : currentMinSdk <= minRequiredV3SdkVersion) {
+                // this satisfies all we need, stop here
+                break;
+            }
+        }
+        if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) {
+            // we can't cover all desired SDK versions, abort
+            throw new InvalidKeyException(
+                    "Provided key algorithms not supported on all desired "
+                            + "Android SDK versions");
+        }
+
+        return processedConfigs;
+    }
+
+    private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
+            boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+        return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
+    }
+
+    private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs(
+            List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) {
+        // The V3.1 signature scheme supports SDK targeted signing config, but this scheme should
+        // only be used when a separate signing config exists for the V3.0 block.
+        if (v3SignerConfigs.size() == 1) {
+            return null;
+        }
+
+        // When there are multiple signing configs, the signer with the minimum SDK version should
+        // be used for the V3.0 block, and all other signers should be used for the V3.1 block.
+        int signerMinSdkVersion = v3SignerConfigs.stream().mapToInt(
+                signer -> signer.minSdkVersion).min().orElse(AndroidSdkVersion.P);
+        List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
+        Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = v3SignerConfigs.iterator();
+        while (v3SignerIterator.hasNext()) {
+            ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
+            // If the signer config's minSdkVersion supports V3.1 and is not the min signer in the
+            // list, then add it to the V3.1 signer configs and remove it from the V3.0 list. If
+            // the signer is targeting the minSdkVersion as a development release, then it should
+            // be included in V3.1 to allow the V3.0 block to target the production release of the
+            // same SDK version.
+            if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+                    && (signerConfig.minSdkVersion > signerMinSdkVersion
+                    || (signerConfig.minSdkVersion >= signerMinSdkVersion
+                            && signerConfig.signerTargetsDevRelease))) {
+                v31SignerConfigs.add(signerConfig);
+                v3SignerIterator.remove();
+            }
+        }
+        return v31SignerConfigs;
+    }
+
+    private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+        List<ApkSigningBlockUtils.SignerConfig> v4Configs = createSigningBlockSignerConfigs(true,
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+        if (v4Configs.size() != 1) {
+            // V4 uses signer config to connect back to v3. Use the same filtering logic.
+            v4Configs = processV3Configs(v4Configs);
+        }
+        List<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs);
+        return new V4SchemeSigner.SignerConfig(v4Configs, v41configs);
+    }
+
+    private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
+            throws InvalidKeyException {
+        ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig(
+                mSourceStampSignerConfig,
+                /* apkSigningBlockPaddingSupported= */ false,
+                ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+        if (mSourceStampSigningCertificateLineage != null) {
+            config.signingCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
+                    config.certificates.get(0));
+        }
+        return config;
+    }
+
+    private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) {
+        int min = Integer.MAX_VALUE;
+        for (SignatureAlgorithm algorithm : algorithms) {
+            int current = algorithm.getMinSdkVersion();
+            if (current < min) {
+                if (current <= mMinSdkVersion || current <= AndroidSdkVersion.P) {
+                    // this algorithm satisfies all of our needs, no need to keep looking
+                    return current;
+                } else {
+                    min = current;
+                }
+            }
+        }
+        return min;
+    }
+
+    private List<ApkSigningBlockUtils.SignerConfig> createSigningBlockSignerConfigs(
+            boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException {
+        List<ApkSigningBlockUtils.SignerConfig> signerConfigs =
+                new ArrayList<>(mSignerConfigs.size() + mTargetedSignerConfigs.size());
+        for (int i = 0; i < mSignerConfigs.size(); i++) {
+            SignerConfig signerConfig = mSignerConfigs.get(i);
+            signerConfigs.add(
+                    createSigningBlockSignerConfig(
+                            signerConfig, apkSigningBlockPaddingSupported, schemeId));
+        }
+        if (schemeId >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+            for (int i = 0; i < mTargetedSignerConfigs.size(); i++) {
+                SignerConfig signerConfig = mTargetedSignerConfigs.get(i);
+                signerConfigs.add(
+                        createSigningBlockSignerConfig(
+                                signerConfig, apkSigningBlockPaddingSupported, schemeId));
+            }
+        }
+        return signerConfigs;
+    }
+
+    private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
+            SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
+            throws InvalidKeyException {
+        List<X509Certificate> certificates = signerConfig.getCertificates();
+        PublicKey publicKey = certificates.get(0).getPublicKey();
+
+        ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
+        newSignerConfig.privateKey = signerConfig.getPrivateKey();
+        newSignerConfig.certificates = certificates;
+        newSignerConfig.minSdkVersion = signerConfig.getMinSdkVersion();
+        newSignerConfig.signerTargetsDevRelease = signerConfig.getSignerTargetsDevRelease();
+        newSignerConfig.signingCertificateLineage = signerConfig.getSigningCertificateLineage();
+
+        switch (schemeId) {
+            case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+                newSignerConfig.signatureAlgorithms =
+                        V2SchemeSigner.getSuggestedSignatureAlgorithms(
+                                publicKey,
+                                mMinSdkVersion,
+                                apkSigningBlockPaddingSupported && mVerityEnabled,
+                                signerConfig.getDeterministicDsaSigning());
+                break;
+            case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+                try {
+                    newSignerConfig.signatureAlgorithms =
+                            V3SchemeSigner.getSuggestedSignatureAlgorithms(
+                                    publicKey,
+                                    mMinSdkVersion,
+                                    apkSigningBlockPaddingSupported && mVerityEnabled,
+                                    signerConfig.getDeterministicDsaSigning());
+                } catch (InvalidKeyException e) {
+
+                    // It is possible for a signer used for v1/v2 signing to not be allowed for use
+                    // with v3 signing.  This is ok as long as there exists a more recent v3 signer
+                    // that covers all supported platform versions.  Populate signatureAlgorithm
+                    // with null, it will be cleaned-up in a later step.
+                    newSignerConfig.signatureAlgorithms = null;
+                }
+                break;
+            case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+                try {
+                    newSignerConfig.signatureAlgorithms =
+                            V4SchemeSigner.getSuggestedSignatureAlgorithms(
+                                    publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported,
+                                    signerConfig.getDeterministicDsaSigning());
+                } catch (InvalidKeyException e) {
+                    // V4 is an optional signing schema, ok to proceed without.
+                    newSignerConfig.signatureAlgorithms = null;
+                }
+                break;
+            case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+                newSignerConfig.signatureAlgorithms =
+                        Collections.singletonList(
+                                SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested");
+        }
+        return newSignerConfig;
+    }
+
+    private boolean isDebuggable(String entryName) {
+        return mDebuggableApkPermitted
+                || !ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName);
+    }
+
+    /**
+     * Initializes DefaultApkSignerEngine with the existing MANIFEST.MF. This reads existing digests
+     * from the MANIFEST.MF file (they are assumed correct) and stores them for the final signature
+     * without recalculation. This step has a significant performance benefit in case of incremental
+     * build.
+     *
+     * <p>This method extracts and stored computed digest for every entry that it would compute it
+     * for in the {@link #outputJarEntry(String)} method
+     *
+     * @param manifestBytes raw representation of MANIFEST.MF file
+     * @param entryNames a set of expected entries names
+     * @return set of entry names which were processed by the engine during the initialization, a
+     *     subset of entryNames
+     */
+    @Override
+    @SuppressWarnings("AndroidJdkLibsChecker")
+    public Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
+        V1SchemeVerifier.Result result = new V1SchemeVerifier.Result();
+        Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
+                V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result);
+        String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
+        for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
+            String entryName = entry.getKey();
+            if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey())
+                    && isDebuggable(entryName)) {
+
+                V1SchemeVerifier.NamedDigest extractedDigest = null;
+                Collection<V1SchemeVerifier.NamedDigest> digestsToVerify =
+                        V1SchemeVerifier.getDigestsToVerify(
+                                entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE);
+                for (V1SchemeVerifier.NamedDigest digestToVerify : digestsToVerify) {
+                    if (digestToVerify.jcaDigestAlgorithm.equals(alg)) {
+                        extractedDigest = digestToVerify;
+                        break;
+                    }
+                }
+                if (extractedDigest != null) {
+                    mOutputJarEntryDigests.put(entryName, extractedDigest.digest);
+                }
+            }
+        }
+        return mOutputJarEntryDigests.keySet();
+    }
+
+    @Override
+    public void setExecutor(RunnablesExecutor executor) {
+        mExecutor = executor;
+    }
+
+    @Override
+    public void inputApkSigningBlock(DataSource apkSigningBlock) {
+        checkNotClosed();
+
+        if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) {
+            return;
+        }
+
+        if (mOtherSignersSignaturesPreserved) {
+            boolean schemeSignatureBlockPreserved = false;
+            mPreservedSignatureBlocks = new ArrayList<>();
+            try {
+                List<Pair<byte[], Integer>> signatureBlocks =
+                        ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock);
+                for (Pair<byte[], Integer> signatureBlock : signatureBlocks) {
+                    if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+                        // If a V2 signature block is found and the engine is configured to use V2
+                        // then save any of the previous signers that are not part of the current
+                        // signing request.
+                        if (mV2SigningEnabled) {
+                            List<Pair<List<X509Certificate>, byte[]>> v2Signers =
+                                    ApkSigningBlockUtils.getApkSignatureBlockSigners(
+                                            signatureBlock.getFirst());
+                            mPreservedV2Signers = new ArrayList<>(v2Signers.size());
+                            for (Pair<List<X509Certificate>, byte[]> v2Signer : v2Signers) {
+                                if (!isConfiguredWithSigner(v2Signer.getFirst())) {
+                                    mPreservedV2Signers.add(v2Signer.getSecond());
+                                    schemeSignatureBlockPreserved = true;
+                                }
+                            }
+                        } else {
+                            // else V2 signing is not enabled; save the entire signature block to be
+                            // added to the final APK signing block.
+                            mPreservedSignatureBlocks.add(signatureBlock);
+                            schemeSignatureBlockPreserved = true;
+                        }
+                    } else if (signatureBlock.getSecond()
+                            == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+                        // Preserving other signers in the presence of a V3 signature block is only
+                        // supported if the engine is configured to resign the APK with the V3
+                        // signature scheme, and the V3 signer in the signature block is the same
+                        // as the engine is configured to use.
+                        if (!mV3SigningEnabled) {
+                            throw new IllegalStateException(
+                                    "Preserving an existing V3 signature is not supported");
+                        }
+                        List<Pair<List<X509Certificate>, byte[]>> v3Signers =
+                                ApkSigningBlockUtils.getApkSignatureBlockSigners(
+                                        signatureBlock.getFirst());
+                        if (v3Signers.size() > 1) {
+                            throw new IllegalArgumentException(
+                                    "The provided APK signing block contains " + v3Signers.size()
+                                            + " V3 signers; the V3 signature scheme only supports"
+                                            + " one signer");
+                        }
+                        // If there is only a single V3 signer then ensure it is the signer
+                        // configured to sign the APK.
+                        if (v3Signers.size() == 1
+                                && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) {
+                            throw new IllegalStateException(
+                                    "The V3 signature scheme only supports one signer; a request "
+                                            + "was made to preserve the existing V3 signature, "
+                                            + "but the engine is configured to sign with a "
+                                            + "different signer");
+                        }
+                    } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains(
+                            signatureBlock.getSecond())) {
+                        mPreservedSignatureBlocks.add(signatureBlock);
+                    }
+                }
+            } catch (ApkFormatException | CertificateException | IOException e) {
+                throw new IllegalArgumentException("Unable to parse the provided signing block", e);
+            }
+            // Signature scheme V3+ only support a single signer; if the engine is configured to
+            // sign with V3+ then ensure no scheme signature blocks have been preserved.
+            if (mV3SigningEnabled && schemeSignatureBlockPreserved) {
+                throw new IllegalStateException(
+                        "Signature scheme V3+ only supports a single signer and cannot be "
+                                + "appended to the existing signature scheme blocks");
+            }
+            return;
+        }
+    }
+
+    /**
+     * Returns whether the engine is configured to sign the APK with a signer using the specified
+     * {@code signerCerts}.
+     */
+    private boolean isConfiguredWithSigner(List<X509Certificate> signerCerts) {
+        for (SignerConfig signerConfig : mSignerConfigs) {
+            if (signerCerts.containsAll(signerConfig.getCertificates())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public InputJarEntryInstructions inputJarEntry(String entryName) {
+        checkNotClosed();
+
+        InputJarEntryInstructions.OutputPolicy outputPolicy =
+                getInputJarEntryOutputPolicy(entryName);
+        switch (outputPolicy) {
+            case SKIP:
+                return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP);
+            case OUTPUT:
+                return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
+            case OUTPUT_BY_ENGINE:
+                if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
+                    // We copy the main section of the JAR manifest from input to output. Thus, this
+                    // invalidates v1 signature and we need to see the entry's data.
+                    mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+                    return new InputJarEntryInstructions(
+                            InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE,
+                            mInputJarManifestEntryDataRequest);
+                }
+                return new InputJarEntryInstructions(
+                        InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE);
+            default:
+                throw new RuntimeException("Unsupported output policy: " + outputPolicy);
+        }
+    }
+
+    @Override
+    public InspectJarEntryRequest outputJarEntry(String entryName) {
+        checkNotClosed();
+        invalidateV2Signature();
+
+        if (!isDebuggable(entryName)) {
+            forgetOutputApkDebuggableStatus();
+        }
+
+        if (!mV1SigningEnabled) {
+            // No need to inspect JAR entries when v1 signing is not enabled.
+            if (!isDebuggable(entryName)) {
+                // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to
+                // check whether it declares that the APK is debuggable
+                mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+                return mOutputAndroidManifestEntryDataRequest;
+            }
+            return null;
+        }
+        // v1 signing is enabled
+
+        if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+            // This entry is covered by v1 signature. We thus need to inspect the entry's data to
+            // compute its digest(s) for v1 signature.
+
+            // TODO: Handle the case where other signer's v1 signatures are present and need to be
+            // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries
+            // covered by v1 signature.
+            invalidateV1Signature();
+            GetJarEntryDataDigestRequest dataDigestRequest =
+                    new GetJarEntryDataDigestRequest(
+                            entryName,
+                            V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+            mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest);
+            mOutputJarEntryDigests.remove(entryName);
+
+            if ((!mDebuggableApkPermitted)
+                    && (ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName))) {
+                // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to
+                // check whether it declares that the APK is debuggable
+                mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+                return new CompoundInspectJarEntryRequest(
+                        entryName, mOutputAndroidManifestEntryDataRequest, dataDigestRequest);
+            }
+
+            return dataDigestRequest;
+        }
+
+        if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+            // This entry is part of v1 signature generated by this engine. We need to check whether
+            // the entry's data is as output by the engine.
+            invalidateV1Signature();
+            GetJarEntryDataRequest dataRequest;
+            if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
+                dataRequest = new GetJarEntryDataRequest(entryName);
+                mInputJarManifestEntryDataRequest = dataRequest;
+            } else {
+                // If this entry is part of v1 signature which has been emitted by this engine,
+                // check whether the output entry's data matches what the engine emitted.
+                dataRequest =
+                        (mEmittedSignatureJarEntryData.containsKey(entryName))
+                                ? new GetJarEntryDataRequest(entryName)
+                                : null;
+            }
+
+            if (dataRequest != null) {
+                mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest);
+            }
+            return dataRequest;
+        }
+
+        // This entry is not covered by v1 signature and isn't part of v1 signature.
+        return null;
+    }
+
+    @Override
+    public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) {
+        checkNotClosed();
+        return getInputJarEntryOutputPolicy(entryName);
+    }
+
+    @Override
+    public void outputJarEntryRemoved(String entryName) {
+        checkNotClosed();
+        invalidateV2Signature();
+        if (!mV1SigningEnabled) {
+            return;
+        }
+
+        if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+            // This entry is covered by v1 signature.
+            invalidateV1Signature();
+            mOutputJarEntryDigests.remove(entryName);
+            mOutputJarEntryDigestRequests.remove(entryName);
+            mOutputSignatureJarEntryDataRequests.remove(entryName);
+            return;
+        }
+
+        if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+            // This entry is part of the v1 signature generated by this engine.
+            invalidateV1Signature();
+            return;
+        }
+    }
+
+    @Override
+    public OutputJarSignatureRequest outputJarEntries()
+            throws ApkFormatException, InvalidKeyException, SignatureException,
+                    NoSuchAlgorithmException {
+        checkNotClosed();
+
+        if (!mV1SignaturePending) {
+            return null;
+        }
+
+        if ((mInputJarManifestEntryDataRequest != null)
+                && (!mInputJarManifestEntryDataRequest.isDone())) {
+            throw new IllegalStateException(
+                    "Still waiting to inspect input APK's "
+                            + mInputJarManifestEntryDataRequest.getEntryName());
+        }
+
+        for (GetJarEntryDataDigestRequest digestRequest : mOutputJarEntryDigestRequests.values()) {
+            String entryName = digestRequest.getEntryName();
+            if (!digestRequest.isDone()) {
+                throw new IllegalStateException(
+                        "Still waiting to inspect output APK's " + entryName);
+            }
+            mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
+        }
+        if (isEligibleForSourceStamp()) {
+            MessageDigest messageDigest =
+                    MessageDigest.getInstance(
+                            V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+            messageDigest.update(generateSourceStampCertificateDigest());
+            mOutputJarEntryDigests.put(
+                    SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+        }
+        mOutputJarEntryDigestRequests.clear();
+
+        for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
+            if (!dataRequest.isDone()) {
+                throw new IllegalStateException(
+                        "Still waiting to inspect output APK's " + dataRequest.getEntryName());
+            }
+        }
+
+        List<Integer> apkSigningSchemeIds = new ArrayList<>();
+        if (mV2SigningEnabled) {
+            apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+        }
+        if (mV3SigningEnabled) {
+            apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+        }
+        byte[] inputJarManifest =
+                (mInputJarManifestEntryDataRequest != null)
+                        ? mInputJarManifestEntryDataRequest.getData()
+                        : null;
+        if (isEligibleForSourceStamp()) {
+            inputJarManifest =
+                    V1SchemeSigner.generateManifestFile(
+                                    mV1ContentDigestAlgorithm,
+                                    mOutputJarEntryDigests,
+                                    inputJarManifest)
+                            .contents;
+        }
+
+        // Check whether the most recently used signature (if present) is still fine.
+        checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+        List<Pair<String, byte[]>> signatureZipEntries;
+        if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) {
+            try {
+                signatureZipEntries =
+                        V1SchemeSigner.sign(
+                                mV1SignerConfigs,
+                                mV1ContentDigestAlgorithm,
+                                mOutputJarEntryDigests,
+                                apkSigningSchemeIds,
+                                inputJarManifest,
+                                mCreatedBy);
+            } catch (CertificateException e) {
+                throw new SignatureException("Failed to generate v1 signature", e);
+            }
+        } else {
+            V1SchemeSigner.OutputManifestFile newManifest =
+                    V1SchemeSigner.generateManifestFile(
+                            mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
+            byte[] emittedSignatureManifest =
+                    mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME);
+            if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
+                // Emitted v1 signature is no longer valid.
+                try {
+                    signatureZipEntries =
+                            V1SchemeSigner.signManifest(
+                                    mV1SignerConfigs,
+                                    mV1ContentDigestAlgorithm,
+                                    apkSigningSchemeIds,
+                                    mCreatedBy,
+                                    newManifest);
+                } catch (CertificateException e) {
+                    throw new SignatureException("Failed to generate v1 signature", e);
+                }
+            } else {
+                // Emitted v1 signature is still valid. Check whether the signature is there in the
+                // output.
+                signatureZipEntries = new ArrayList<>();
+                for (Map.Entry<String, byte[]> expectedOutputEntry :
+                        mEmittedSignatureJarEntryData.entrySet()) {
+                    String entryName = expectedOutputEntry.getKey();
+                    byte[] expectedData = expectedOutputEntry.getValue();
+                    GetJarEntryDataRequest actualDataRequest =
+                            mOutputSignatureJarEntryDataRequests.get(entryName);
+                    if (actualDataRequest == null) {
+                        // This signature entry hasn't been output.
+                        signatureZipEntries.add(Pair.of(entryName, expectedData));
+                        continue;
+                    }
+                    byte[] actualData = actualDataRequest.getData();
+                    if (!Arrays.equals(expectedData, actualData)) {
+                        signatureZipEntries.add(Pair.of(entryName, expectedData));
+                    }
+                }
+                if (signatureZipEntries.isEmpty()) {
+                    // v1 signature in the output is valid
+                    return null;
+                }
+                // v1 signature in the output is not valid.
+            }
+        }
+
+        if (signatureZipEntries.isEmpty()) {
+            // v1 signature in the output is valid
+            mV1SignaturePending = false;
+            return null;
+        }
+
+        List<OutputJarSignatureRequest.JarEntry> sigEntries =
+                new ArrayList<>(signatureZipEntries.size());
+        for (Pair<String, byte[]> entry : signatureZipEntries) {
+            String entryName = entry.getFirst();
+            byte[] entryData = entry.getSecond();
+            sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData));
+            mEmittedSignatureJarEntryData.put(entryName, entryData);
+        }
+        mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries);
+        return mAddV1SignatureRequest;
+    }
+
+    @Deprecated
+    @Override
+    public OutputApkSigningBlockRequest outputZipSections(
+            DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+            throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
+        return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false);
+    }
+
+    @Override
+    public OutputApkSigningBlockRequest2 outputZipSections2(
+            DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+            throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
+        return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true);
+    }
+
+    private OutputApkSigningBlockRequestImpl outputZipSectionsInternal(
+            DataSource zipEntries,
+            DataSource zipCentralDirectory,
+            DataSource zipEocd,
+            boolean apkSigningBlockPaddingSupported)
+            throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
+        checkNotClosed();
+        checkV1SigningDoneIfEnabled();
+        if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) {
+            return null;
+        }
+        checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+
+        // adjust to proper padding
+        Pair<DataSource, Integer> paddingPair =
+                ApkSigningBlockUtils.generateApkSigningBlockPadding(
+                        zipEntries, apkSigningBlockPaddingSupported);
+        DataSource beforeCentralDir = paddingPair.getFirst();
+        int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
+        DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
+
+        List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>();
+        ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
+        ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
+        // If the engine is configured to preserve previous signature blocks and any were found in
+        // the existing APK signing block then add them to the list to be used to generate the
+        // new APK signing block.
+        if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null
+                && !mPreservedSignatureBlocks.isEmpty()) {
+            signingSchemeBlocks.addAll(mPreservedSignatureBlocks);
+        }
+
+        // create APK Signature Scheme V2 Signature if requested
+        if (mV2SigningEnabled) {
+            invalidateV2Signature();
+            List<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs =
+                    createV2SignerConfigs(apkSigningBlockPaddingSupported);
+            v2SigningSchemeBlockAndDigests =
+                    V2SchemeSigner.generateApkSignatureSchemeV2Block(
+                            mExecutor,
+                            beforeCentralDir,
+                            zipCentralDirectory,
+                            eocd,
+                            v2SignerConfigs,
+                            mV3SigningEnabled,
+                            mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null);
+            signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
+        }
+        if (mV3SigningEnabled) {
+            invalidateV3Signature();
+            List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
+                    createV3SignerConfigs(apkSigningBlockPaddingSupported);
+            List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs(
+                    v3SignerConfigs);
+            if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) {
+                ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+                        v31SigningSchemeBlockAndDigests =
+                        new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd,
+                                v31SignerConfigs)
+                                .setRunnablesExecutor(mExecutor)
+                                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+                                .build()
+                                .generateApkSignatureSchemeV3BlockAndDigests();
+                signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
+            }
+            V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir,
+                zipCentralDirectory, eocd, v3SignerConfigs)
+                .setRunnablesExecutor(mExecutor)
+                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+            if (v31SignerConfigs != null && !v31SignerConfigs.isEmpty()) {
+                // The V3.1 stripping protection writes the minimum SDK version from the targeted
+                // signers as an additional attribute in the V3.0 signing block.
+                int minSdkVersionForV31 = v31SignerConfigs.stream().mapToInt(
+                        signer -> signer.minSdkVersion).min().orElse(MIN_SDK_WITH_V31_SUPPORT);
+                builder.setMinSdkVersionForV31(minSdkVersionForV31);
+            }
+            v3SigningSchemeBlockAndDigests =
+                builder.build().generateApkSignatureSchemeV3BlockAndDigests();
+            signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
+        }
+        if (isEligibleForSourceStamp()) {
+            ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
+                    createSourceStampSignerConfig();
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos =
+                    new HashMap<>();
+            if (mV3SigningEnabled) {
+                signatureSchemeDigestInfos.put(
+                        VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo);
+            }
+            if (mV2SigningEnabled) {
+                signatureSchemeDigestInfos.put(
+                        VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo);
+            }
+            if (mV1SigningEnabled) {
+                Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>();
+                try {
+                    // Jar signing related variables must have been already populated at this point
+                    // if V1 signing is enabled since it is happening before computations on the APK
+                    // signing block (V2/V3/V4/SourceStamp signing).
+                    byte[] inputJarManifest =
+                            (mInputJarManifestEntryDataRequest != null)
+                                    ? mInputJarManifestEntryDataRequest.getData()
+                                    : null;
+                    byte[] jarManifest =
+                            V1SchemeSigner.generateManifestFile(
+                                            mV1ContentDigestAlgorithm,
+                                            mOutputJarEntryDigests,
+                                            inputJarManifest)
+                                    .contents;
+                    // The digest of the jar manifest does not need to be computed in chunks due to
+                    // the small size of the manifest.
+                    v1SigningSchemeDigests.put(
+                            ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest));
+                } catch (ApkFormatException e) {
+                    throw new RuntimeException("Failed to generate manifest file", e);
+                }
+                signatureSchemeDigestInfos.put(
+                        VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
+            }
+            V2SourceStampSigner v2SourceStampSigner =
+                    new V2SourceStampSigner.Builder(sourceStampSignerConfig,
+                            signatureSchemeDigestInfos)
+                            .setSourceStampTimestampEnabled(mSourceStampTimestampEnabled)
+                            .build();
+            signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock());
+        }
+
+        // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks
+        byte[] apkSigningBlock = ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
+
+        mAddSigningBlockRequest =
+                new OutputApkSigningBlockRequestImpl(apkSigningBlock, padSizeBeforeApkSigningBlock);
+        return mAddSigningBlockRequest;
+    }
+
+    @Override
+    public void outputDone() {
+        checkNotClosed();
+        checkV1SigningDoneIfEnabled();
+        checkSigningBlockDoneIfEnabled();
+    }
+
+    @Override
+    public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+            throws SignatureException {
+        if (outputFile == null) {
+            if (ignoreFailures) {
+                return;
+            }
+            throw new SignatureException("Missing V4 output file.");
+        }
+        try {
+            V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+            V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
+        } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+            if (ignoreFailures) {
+                return;
+            }
+            throw new SignatureException("V4 signing failed", e);
+        }
+    }
+
+    /** For external use only to generate V4 & tree separately. */
+    public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput)
+            throws SignatureException {
+        if (sigOutput == null) {
+            throw new SignatureException("Missing V4 output streams.");
+        }
+        try {
+            V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+            Pair<V4Signature, byte[]> pair =
+                    V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
+            pair.getFirst().writeTo(sigOutput);
+            return pair.getSecond();
+        } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+            throw new SignatureException("V4 signing failed", e);
+        }
+    }
+
+    @Override
+    public boolean isEligibleForSourceStamp() {
+        return mSourceStampSignerConfig != null
+                && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled);
+    }
+
+    @Override
+    public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+        if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+            throw new SignatureException("No certificates configured for stamp");
+        }
+        try {
+            return computeSha256DigestBytes(
+                    mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException("Failed to encode source stamp certificate", e);
+        }
+    }
+
+    @Override
+    public void close() {
+        mClosed = true;
+
+        mAddV1SignatureRequest = null;
+        mInputJarManifestEntryDataRequest = null;
+        mOutputAndroidManifestEntryDataRequest = null;
+        mDebuggable = null;
+        mOutputJarEntryDigestRequests.clear();
+        mOutputJarEntryDigests.clear();
+        mEmittedSignatureJarEntryData.clear();
+        mOutputSignatureJarEntryDataRequests.clear();
+
+        mAddSigningBlockRequest = null;
+    }
+
+    private void invalidateV1Signature() {
+        if (mV1SigningEnabled) {
+            mV1SignaturePending = true;
+        }
+        invalidateV2Signature();
+    }
+
+    private void invalidateV2Signature() {
+        if (mV2SigningEnabled) {
+            mV2SignaturePending = true;
+            mAddSigningBlockRequest = null;
+        }
+    }
+
+    private void invalidateV3Signature() {
+        if (mV3SigningEnabled) {
+            mV3SignaturePending = true;
+            mAddSigningBlockRequest = null;
+        }
+    }
+
+    private void checkNotClosed() {
+        if (mClosed) {
+            throw new IllegalStateException("Engine closed");
+        }
+    }
+
+    private void checkV1SigningDoneIfEnabled() {
+        if (!mV1SignaturePending) {
+            return;
+        }
+
+        if (mAddV1SignatureRequest == null) {
+            throw new IllegalStateException(
+                    "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?");
+        }
+        if (!mAddV1SignatureRequest.isDone()) {
+            throw new IllegalStateException(
+                    "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
+                            + " been fulfilled");
+        }
+        for (Map.Entry<String, byte[]> expectedOutputEntry :
+                mEmittedSignatureJarEntryData.entrySet()) {
+            String entryName = expectedOutputEntry.getKey();
+            byte[] expectedData = expectedOutputEntry.getValue();
+            GetJarEntryDataRequest actualDataRequest =
+                    mOutputSignatureJarEntryDataRequests.get(entryName);
+            if (actualDataRequest == null) {
+                throw new IllegalStateException(
+                        "APK entry "
+                                + entryName
+                                + " not yet output despite this having been"
+                                + " requested");
+            } else if (!actualDataRequest.isDone()) {
+                throw new IllegalStateException(
+                        "Still waiting to inspect output APK's " + entryName);
+            }
+            byte[] actualData = actualDataRequest.getData();
+            if (!Arrays.equals(expectedData, actualData)) {
+                throw new IllegalStateException(
+                        "Output APK entry " + entryName + " data differs from what was requested");
+            }
+        }
+        mV1SignaturePending = false;
+    }
+
+    private void checkSigningBlockDoneIfEnabled() {
+        if (!mV2SignaturePending && !mV3SignaturePending) {
+            return;
+        }
+        if (mAddSigningBlockRequest == null) {
+            throw new IllegalStateException(
+                    "Signed APK Signing BLock not yet generated. Skipped outputZipSections()?");
+        }
+        if (!mAddSigningBlockRequest.isDone()) {
+            throw new IllegalStateException(
+                    "APK Signing Block addition of signature(s) requested by"
+                            + " outputZipSections() hasn't been fulfilled yet");
+        }
+        mAddSigningBlockRequest = null;
+        mV2SignaturePending = false;
+        mV3SignaturePending = false;
+    }
+
+    private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected() throws SignatureException {
+        if (mDebuggableApkPermitted) {
+            return;
+        }
+
+        try {
+            if (isOutputApkDebuggable()) {
+                throw new SignatureException(
+                        "APK is debuggable (see android:debuggable attribute) and this engine is"
+                                + " configured to refuse to sign debuggable APKs");
+            }
+        } catch (ApkFormatException e) {
+            throw new SignatureException("Failed to determine whether the APK is debuggable", e);
+        }
+    }
+
+    /**
+     * Returns whether the output APK is debuggable according to its {@code android:debuggable}
+     * declaration.
+     */
+    private boolean isOutputApkDebuggable() throws ApkFormatException {
+        if (mDebuggable != null) {
+            return mDebuggable;
+        }
+
+        if (mOutputAndroidManifestEntryDataRequest == null) {
+            throw new IllegalStateException(
+                    "Cannot determine debuggable status of output APK because "
+                            + ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME
+                            + " entry contents have not yet been requested");
+        }
+
+        if (!mOutputAndroidManifestEntryDataRequest.isDone()) {
+            throw new IllegalStateException(
+                    "Still waiting to inspect output APK's "
+                            + mOutputAndroidManifestEntryDataRequest.getEntryName());
+        }
+        mDebuggable =
+                ApkUtils.getDebuggableFromBinaryAndroidManifest(
+                        ByteBuffer.wrap(mOutputAndroidManifestEntryDataRequest.getData()));
+        return mDebuggable;
+    }
+
+    private void forgetOutputApkDebuggableStatus() {
+        mDebuggable = null;
+    }
+
+    /** Returns the output policy for the provided input JAR entry. */
+    private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
+        if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+            return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
+        }
+        if ((mOtherSignersSignaturesPreserved)
+                || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) {
+            return InputJarEntryInstructions.OutputPolicy.OUTPUT;
+        }
+        return InputJarEntryInstructions.OutputPolicy.SKIP;
+    }
+
+    private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
+        private final List<JarEntry> mAdditionalJarEntries;
+        private volatile boolean mDone;
+
+        private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) {
+            mAdditionalJarEntries =
+                    Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
+        }
+
+        @Override
+        public List<JarEntry> getAdditionalJarEntries() {
+            return mAdditionalJarEntries;
+        }
+
+        @Override
+        public void done() {
+            mDone = true;
+        }
+
+        private boolean isDone() {
+            return mDone;
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private static class OutputApkSigningBlockRequestImpl
+            implements OutputApkSigningBlockRequest, OutputApkSigningBlockRequest2 {
+        private final byte[] mApkSigningBlock;
+        private final int mPaddingBeforeApkSigningBlock;
+        private volatile boolean mDone;
+
+        private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock, int paddingBefore) {
+            mApkSigningBlock = apkSigingBlock.clone();
+            mPaddingBeforeApkSigningBlock = paddingBefore;
+        }
+
+        @Override
+        public byte[] getApkSigningBlock() {
+            return mApkSigningBlock.clone();
+        }
+
+        @Override
+        public void done() {
+            mDone = true;
+        }
+
+        private boolean isDone() {
+            return mDone;
+        }
+
+        @Override
+        public int getPaddingSizeBeforeApkSigningBlock() {
+            return mPaddingBeforeApkSigningBlock;
+        }
+    }
+
+    /** JAR entry inspection request which obtain the entry's uncompressed data. */
+    private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
+        private final String mEntryName;
+        private final Object mLock = new Object();
+
+        private boolean mDone;
+        private DataSink mDataSink;
+        private ByteArrayOutputStream mDataSinkBuf;
+
+        private GetJarEntryDataRequest(String entryName) {
+            mEntryName = entryName;
+        }
+
+        @Override
+        public String getEntryName() {
+            return mEntryName;
+        }
+
+        @Override
+        public DataSink getDataSink() {
+            synchronized (mLock) {
+                checkNotDone();
+                if (mDataSinkBuf == null) {
+                    mDataSinkBuf = new ByteArrayOutputStream();
+                }
+                if (mDataSink == null) {
+                    mDataSink = DataSinks.asDataSink(mDataSinkBuf);
+                }
+                return mDataSink;
+            }
+        }
+
+        @Override
+        public void done() {
+            synchronized (mLock) {
+                if (mDone) {
+                    return;
+                }
+                mDone = true;
+            }
+        }
+
+        private boolean isDone() {
+            synchronized (mLock) {
+                return mDone;
+            }
+        }
+
+        private void checkNotDone() throws IllegalStateException {
+            synchronized (mLock) {
+                if (mDone) {
+                    throw new IllegalStateException("Already done");
+                }
+            }
+        }
+
+        private byte[] getData() {
+            synchronized (mLock) {
+                if (!mDone) {
+                    throw new IllegalStateException("Not yet done");
+                }
+                return (mDataSinkBuf != null) ? mDataSinkBuf.toByteArray() : new byte[0];
+            }
+        }
+    }
+
+    /** JAR entry inspection request which obtains the digest of the entry's uncompressed data. */
+    private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
+        private final String mEntryName;
+        private final String mJcaDigestAlgorithm;
+        private final Object mLock = new Object();
+
+        private boolean mDone;
+        private DataSink mDataSink;
+        private MessageDigest mMessageDigest;
+        private byte[] mDigest;
+
+        private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) {
+            mEntryName = entryName;
+            mJcaDigestAlgorithm = jcaDigestAlgorithm;
+        }
+
+        @Override
+        public String getEntryName() {
+            return mEntryName;
+        }
+
+        @Override
+        public DataSink getDataSink() {
+            synchronized (mLock) {
+                checkNotDone();
+                if (mDataSink == null) {
+                    mDataSink = DataSinks.asDataSink(getMessageDigest());
+                }
+                return mDataSink;
+            }
+        }
+
+        private MessageDigest getMessageDigest() {
+            synchronized (mLock) {
+                if (mMessageDigest == null) {
+                    try {
+                        mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm);
+                    } catch (NoSuchAlgorithmException e) {
+                        throw new RuntimeException(
+                                mJcaDigestAlgorithm + " MessageDigest not available", e);
+                    }
+                }
+                return mMessageDigest;
+            }
+        }
+
+        @Override
+        public void done() {
+            synchronized (mLock) {
+                if (mDone) {
+                    return;
+                }
+                mDone = true;
+                mDigest = getMessageDigest().digest();
+                mMessageDigest = null;
+                mDataSink = null;
+            }
+        }
+
+        private boolean isDone() {
+            synchronized (mLock) {
+                return mDone;
+            }
+        }
+
+        private void checkNotDone() throws IllegalStateException {
+            synchronized (mLock) {
+                if (mDone) {
+                    throw new IllegalStateException("Already done");
+                }
+            }
+        }
+
+        private byte[] getDigest() {
+            synchronized (mLock) {
+                if (!mDone) {
+                    throw new IllegalStateException("Not yet done");
+                }
+                return mDigest.clone();
+            }
+        }
+    }
+
+    /** JAR entry inspection request which transparently satisfies multiple such requests. */
+    private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest {
+        private final String mEntryName;
+        private final InspectJarEntryRequest[] mRequests;
+        private final Object mLock = new Object();
+
+        private DataSink mSink;
+
+        private CompoundInspectJarEntryRequest(
+                String entryName, InspectJarEntryRequest... requests) {
+            mEntryName = entryName;
+            mRequests = requests;
+        }
+
+        @Override
+        public String getEntryName() {
+            return mEntryName;
+        }
+
+        @Override
+        public DataSink getDataSink() {
+            synchronized (mLock) {
+                if (mSink == null) {
+                    DataSink[] sinks = new DataSink[mRequests.length];
+                    for (int i = 0; i < sinks.length; i++) {
+                        sinks[i] = mRequests[i].getDataSink();
+                    }
+                    mSink = new TeeDataSink(sinks);
+                }
+                return mSink;
+            }
+        }
+
+        @Override
+        public void done() {
+            for (InspectJarEntryRequest request : mRequests) {
+                request.done();
+            }
+        }
+    }
+
+    /**
+     * Configuration of a signer.
+     *
+     * <p>Use {@link Builder} to obtain configuration instances.
+     */
+    public static class SignerConfig {
+        private final String mName;
+        private final PrivateKey mPrivateKey;
+        private final List<X509Certificate> mCertificates;
+        private final boolean mDeterministicDsaSigning;
+        private final int mMinSdkVersion;
+        private final boolean mSignerTargetsDevRelease;
+        private final SigningCertificateLineage mSigningCertificateLineage;
+
+        private SignerConfig(Builder builder) {
+            mName = builder.mName;
+            mPrivateKey = builder.mPrivateKey;
+            mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
+            mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
+            mMinSdkVersion = builder.mMinSdkVersion;
+            mSignerTargetsDevRelease = builder.mSignerTargetsDevRelease;
+            mSigningCertificateLineage = builder.mSigningCertificateLineage;
+        }
+
+        /** Returns the name of this signer. */
+        public String getName() {
+            return mName;
+        }
+
+        /** Returns the signing key of this signer. */
+        public PrivateKey getPrivateKey() {
+            return mPrivateKey;
+        }
+
+        /**
+         * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+         * to this signer's private key.
+         */
+        public List<X509Certificate> getCertificates() {
+            return mCertificates;
+        }
+
+        /**
+         * If this signer is a DSA signer, whether or not the signing is done deterministically.
+         */
+        public boolean getDeterministicDsaSigning() {
+            return mDeterministicDsaSigning;
+        }
+
+        /** Returns the minimum SDK version for which this signer should be used. */
+        public int getMinSdkVersion() {
+            return mMinSdkVersion;
+        }
+
+        /** Returns whether this signer targets a development release. */
+        public boolean getSignerTargetsDevRelease() {
+            return mSignerTargetsDevRelease;
+        }
+
+        /** Returns the {@link SigningCertificateLineage} for this signer. */
+        public SigningCertificateLineage getSigningCertificateLineage() {
+            return mSigningCertificateLineage;
+        }
+
+        /** Builder of {@link SignerConfig} instances. */
+        public static class Builder {
+            private final String mName;
+            private final PrivateKey mPrivateKey;
+            private final List<X509Certificate> mCertificates;
+            private final boolean mDeterministicDsaSigning;
+            private int mMinSdkVersion;
+            private boolean mSignerTargetsDevRelease;
+            private SigningCertificateLineage mSigningCertificateLineage;
+
+            /**
+             * Constructs a new {@code Builder}.
+             *
+             * @param name signer's name. The name is reflected in the name of files comprising the
+             *     JAR signature of the APK.
+             * @param privateKey signing key
+             * @param certificates list of one or more X.509 certificates. The subject public key of
+             *     the first certificate must correspond to the {@code privateKey}.
+             */
+            public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+                this(name, privateKey, certificates, false);
+            }
+
+            /**
+             * Constructs a new {@code Builder}.
+             *
+             * @param name signer's name. The name is reflected in the name of files comprising the
+             *     JAR signature of the APK.
+             * @param privateKey signing key
+             * @param certificates list of one or more X.509 certificates. The subject public key of
+             *     the first certificate must correspond to the {@code privateKey}.
+             * @param deterministicDsaSigning When signing using DSA, whether or not the
+             * deterministic signing algorithm variant (RFC6979) should be used.
+             */
+            public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates,
+                    boolean deterministicDsaSigning) {
+                if (name.isEmpty()) {
+                    throw new IllegalArgumentException("Empty name");
+                }
+                mName = name;
+                mPrivateKey = privateKey;
+                mCertificates = new ArrayList<>(certificates);
+                mDeterministicDsaSigning = deterministicDsaSigning;
+            }
+
+            /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
+            public Builder setMinSdkVersion(int minSdkVersion) {
+                return setLineageForMinSdkVersion(null, minSdkVersion);
+            }
+
+            /**
+             * Sets the specified {@code minSdkVersion} as the minimum Android platform version
+             * (API level) for which the provided {@code lineage} (where applicable) should be used
+             * to produce the APK's signature. This method is useful if callers want to specify a
+             * particular rotated signer or lineage with restricted capabilities for later
+             * platform releases.
+             *
+             * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
+             * signing lineages with capabilities; only an app's original signer(s) can be used for
+             * the V1 and V2 signature blocks. Because of this, only a value of {@code
+             * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
+             * introduced can be specified.
+             *
+             * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
+             * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
+             * the current {@code SignerConfig} being used in the V3.0 signing block and applied to
+             * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
+             * subsequent {@code SignerConfig} instances). Because of this, only a single {@code
+             * SignerConfig} can be instantiated with a minimum SDK version <= 32.
+             *
+             * @param lineage the {@code SigningCertificateLineage} to target the specified {@code
+             *                minSdkVersion}
+             * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
+             *                      should be used
+             * @return this {@code Builder} instance
+             *
+             * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
+             * certificate provided in the constructor is not in the specified {@code lineage}.
+             */
+            public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
+                    int minSdkVersion) {
+                if (minSdkVersion < AndroidSdkVersion.P) {
+                    throw new IllegalArgumentException(
+                            "SDK targeted signing config is only supported with the V3 signature "
+                                    + "scheme on Android P (SDK version "
+                                    + AndroidSdkVersion.P + ") and later");
+                }
+                if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                    minSdkVersion = AndroidSdkVersion.P;
+                }
+                mMinSdkVersion = minSdkVersion;
+                // If a lineage is provided, ensure the signing certificate for this signer is in
+                // the lineage; in the case of multiple signing certificates, the first is always
+                // used in the lineage.
+                if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
+                    throw new IllegalArgumentException(
+                            "The provided lineage does not contain the signing certificate, "
+                                    + mCertificates.get(0).getSubjectDN()
+                                    + ", for this SignerConfig");
+                }
+                mSigningCertificateLineage = lineage;
+                return this;
+            }
+
+            /**
+             * Sets whether this signer's min SDK version is intended to target a development
+             * release.
+             *
+             * <p>This is primarily required for a signer testing on a platform's development
+             * release; however, it is recommended that signer's use the latest development SDK
+             * version instead of explicitly specifying this boolean. This class will properly
+             * handle an SDK that is currently targeting a development release and will use the
+             * finalized SDK version on release.
+             */
+            private Builder setSignerTargetsDevRelease(boolean signerTargetsDevRelease) {
+                if (signerTargetsDevRelease && mMinSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                    throw new IllegalArgumentException(
+                            "Rotation can only target a development release for signers targeting "
+                                    + MIN_SDK_WITH_V31_SUPPORT + " or later");
+                }
+                mSignerTargetsDevRelease = signerTargetsDevRelease;
+                return this;
+            }
+
+
+            /**
+             * Returns a new {@code SignerConfig} instance configured based on the configuration of
+             * this builder.
+             */
+            public SignerConfig build() {
+                return new SignerConfig(this);
+            }
+        }
+    }
+
+    /** Builder of {@link DefaultApkSignerEngine} instances. */
+    public static class Builder {
+        private List<SignerConfig> mSignerConfigs;
+        private List<SignerConfig> mTargetedSignerConfigs;
+        private SignerConfig mStampSignerConfig;
+        private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+        private boolean mSourceStampTimestampEnabled = true;
+        private final int mMinSdkVersion;
+
+        private boolean mV1SigningEnabled = true;
+        private boolean mV2SigningEnabled = true;
+        private boolean mV3SigningEnabled = true;
+        private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+        private boolean mRotationTargetsDevRelease = false;
+        private boolean mVerityEnabled = false;
+        private boolean mDebuggableApkPermitted = true;
+        private boolean mOtherSignersSignaturesPreserved;
+        private String mCreatedBy = "1.0 (Android)";
+
+        private SigningCertificateLineage mSigningCertificateLineage;
+
+        // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
+        // signing by default, but not require prior clients to update to explicitly disable v3
+        // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided
+        // inputs (multiple signers and mSigningCertificateLineage in particular).  Maintain two
+        // extra variables to record whether or not mV3SigningEnabled has been set directly by a
+        // client and so should override the default behavior.
+        private boolean mV3SigningExplicitlyDisabled = false;
+        private boolean mV3SigningExplicitlyEnabled = false;
+
+        /**
+         * Constructs a new {@code Builder}.
+         *
+         * @param signerConfigs information about signers with which the APK will be signed. At
+         *     least one signer configuration must be provided.
+         * @param minSdkVersion API Level of the oldest Android platform on which the APK is
+         *     supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code
+         *     AndroidManifest.xml}. The higher the version, the stronger signing features will be
+         *     enabled.
+         */
+        public Builder(List<SignerConfig> signerConfigs, int minSdkVersion) {
+            if (signerConfigs.isEmpty()) {
+                throw new IllegalArgumentException("At least one signer config must be provided");
+            }
+            if (signerConfigs.size() > 1) {
+                // APK Signature Scheme v3 only supports single signer, unless a
+                // SigningCertificateLineage is provided, in which case this will be reset to true,
+                // since we don't yet have a v4 scheme about which to worry
+                mV3SigningEnabled = false;
+            }
+            mSignerConfigs = new ArrayList<>(signerConfigs);
+            mMinSdkVersion = minSdkVersion;
+        }
+
+        /**
+         * Sets the APK signature schemes that should be enabled based on the options provided by
+         * the caller.
+         */
+        private void setEnabledSignatureSchemes() {
+            if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
+                throw new IllegalStateException(
+                        "Builder configured to both enable and disable APK "
+                                + "Signature Scheme v3 signing");
+            }
+            if (mV3SigningExplicitlyDisabled) {
+                mV3SigningEnabled = false;
+            } else if (mV3SigningExplicitlyEnabled) {
+                mV3SigningEnabled = true;
+            }
+        }
+
+        /**
+         * Sets the SDK targeted signer configs based on the signing config and rotation options
+         * provided by the caller.
+         *
+         * @throws InvalidKeyException if a {@link SigningCertificateLineage} cannot be created
+         * from the provided options
+         */
+        private void setTargetedSignerConfigs() throws InvalidKeyException {
+            // If the caller specified any SDK targeted signer configs, then the min SDK version
+            // should be set for those configs, all others should have a default 0 min SDK version.
+            mSignerConfigs.sort(((signerConfig1, signerConfig2) -> signerConfig1.getMinSdkVersion()
+                    - signerConfig2.getMinSdkVersion()));
+            // With the signer configs sorted, find the first targeted signer config with a min
+            // SDK version > 0 to create the separate targeted signer configs.
+            mTargetedSignerConfigs = new ArrayList<>();
+            for (int i = 0; i < mSignerConfigs.size(); i++) {
+                if (mSignerConfigs.get(i).getMinSdkVersion() > 0) {
+                    mTargetedSignerConfigs = mSignerConfigs.subList(i, mSignerConfigs.size());
+                    mSignerConfigs = mSignerConfigs.subList(0, i);
+                    break;
+                }
+            }
+
+            // A lineage provided outside a targeted signing config is intended for the original
+            // rotation; sort the untargeted signing configs based on this lineage and create a new
+            // targeted signing config for the initial rotation.
+            if (mSigningCertificateLineage != null) {
+                if (!mTargetedSignerConfigs.isEmpty()) {
+                    // Only the initial rotation can use the rotation-min-sdk-version; all
+                    // subsequent targeted rotations must use targeted signing configs.
+                    int firstTargetedSdkVersion = mTargetedSignerConfigs.get(0).getMinSdkVersion();
+                    if (mRotationMinSdkVersion >= firstTargetedSdkVersion) {
+                        throw new IllegalStateException(
+                                "The rotation-min-sdk-version, " + mRotationMinSdkVersion
+                                        + ", must be less than the first targeted SDK version, "
+                                        + firstTargetedSdkVersion);
+                    }
+                }
+                try {
+                    mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs);
+                } catch (IllegalArgumentException e) {
+                    throw new IllegalStateException(
+                            "Provided signer configs do not match the "
+                                    + "provided SigningCertificateLineage",
+                            e);
+                }
+                // Get the last signer in the lineage, create a new targeted signer from it,
+                // and add it as a targeted signer config.
+                SignerConfig rotatedSignerConfig = mSignerConfigs.remove(mSignerConfigs.size() - 1);
+                SignerConfig.Builder rotatedConfigBuilder = new SignerConfig.Builder(
+                        rotatedSignerConfig.getName(), rotatedSignerConfig.getPrivateKey(),
+                        rotatedSignerConfig.getCertificates(),
+                        rotatedSignerConfig.getDeterministicDsaSigning());
+                rotatedConfigBuilder.setLineageForMinSdkVersion(mSigningCertificateLineage,
+                        mRotationMinSdkVersion);
+                rotatedConfigBuilder.setSignerTargetsDevRelease(mRotationTargetsDevRelease);
+                mTargetedSignerConfigs.add(0, rotatedConfigBuilder.build());
+            }
+            mSigningCertificateLineage = mergeTargetedSigningConfigLineages();
+        }
+
+        /**
+         * Merges and returns the lineages from any caller provided SDK targeted {@link
+         * SignerConfig} instances with an optional {@code lineage} specified as part of the general
+         * signing config.
+         *
+         * <p>If multiple signing configs target the same SDK version, or if any of the lineages
+         * cannot be merged, then an {@code IllegalStateException} is thrown.
+         */
+        private SigningCertificateLineage mergeTargetedSigningConfigLineages()
+                throws InvalidKeyException {
+            SigningCertificateLineage mergedLineage = null;
+            int prevSdkVersion = 0;
+            for (SignerConfig signerConfig : mTargetedSignerConfigs) {
+                int signerMinSdkVersion = signerConfig.getMinSdkVersion();
+                if (signerMinSdkVersion < AndroidSdkVersion.P) {
+                    throw new IllegalStateException(
+                            "Targeted signing config is not supported prior to SDK version "
+                                    + AndroidSdkVersion.P + "; received value "
+                                    + signerMinSdkVersion);
+                }
+                SigningCertificateLineage signerLineage =
+                        signerConfig.getSigningCertificateLineage();
+                // It is possible for a lineage to be null if the user is using one of the
+                // signers from the lineage as the only signer to target an SDK version; create
+                // a single element lineage to verify the signer is part of the merged lineage.
+                if (signerLineage == null) {
+                    try {
+                        signerLineage = new SigningCertificateLineage.Builder(
+                                new SigningCertificateLineage.SignerConfig.Builder(
+                                        signerConfig.mPrivateKey,
+                                        signerConfig.mCertificates.get(0))
+                                        .build())
+                                .build();
+                    } catch (CertificateEncodingException | NoSuchAlgorithmException
+                            | SignatureException e) {
+                        throw new IllegalStateException(
+                                "Unable to create a SignerConfig for signer from certificate "
+                                        + signerConfig.mCertificates.get(0).getSubjectDN());
+                    }
+                }
+                // The V3.0 signature scheme does not support verified targeted SDK signing
+                // configs; if a signer is targeting any SDK version < T, then it will
+                // target P with the V3.0 signature scheme.
+                if (signerMinSdkVersion < AndroidSdkVersion.T) {
+                    signerMinSdkVersion = AndroidSdkVersion.P;
+                }
+                // Ensure there are no SignerConfigs targeting the same SDK version.
+                if (signerMinSdkVersion == prevSdkVersion) {
+                    throw new IllegalStateException(
+                            "Multiple SignerConfigs were found targeting SDK version "
+                                    + signerMinSdkVersion);
+                }
+                // If multiple lineages have been provided, then verify each subsequent lineage
+                // is a valid descendant or ancestor of the previously merged lineages.
+                if (mergedLineage == null) {
+                    mergedLineage = signerLineage;
+                } else {
+                    try {
+                        mergedLineage = mergedLineage.mergeLineageWith(signerLineage);
+                    } catch (IllegalArgumentException e) {
+                        throw new IllegalStateException(
+                                "The provided lineage targeting SDK " + signerMinSdkVersion
+                                        + " is not in the signing history of the other targeted "
+                                        + "signing configs", e);
+                    }
+                }
+                prevSdkVersion = signerMinSdkVersion;
+            }
+            return mergedLineage;
+        }
+
+        /**
+         * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
+         * configuration of this builder.
+         */
+        public DefaultApkSignerEngine build() throws InvalidKeyException {
+            setEnabledSignatureSchemes();
+            setTargetedSignerConfigs();
+
+            // make sure our signers are appropriately setup
+            if (mSigningCertificateLineage != null) {
+                if (!mV3SigningEnabled && mSignerConfigs.size() > 1) {
+                    // this is a strange situation: we've provided a valid rotation history, but
+                    // are only signing with v1/v2.  blow up, since we don't know for sure with
+                    // which signer the user intended to sign
+                    throw new IllegalStateException(
+                            "Provided multiple signers which are part of the"
+                                    + " SigningCertificateLineage, but not signing with APK"
+                                    + " Signature Scheme v3");
+                }
+            } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) {
+                throw new IllegalStateException(
+                        "Multiple signing certificates provided for use with APK Signature Scheme"
+                                + " v3 without an accompanying SigningCertificateLineage");
+            }
+
+            return new DefaultApkSignerEngine(
+                    mSignerConfigs,
+                    mTargetedSignerConfigs,
+                    mStampSignerConfig,
+                    mSourceStampSigningCertificateLineage,
+                    mSourceStampTimestampEnabled,
+                    mMinSdkVersion,
+                    mV1SigningEnabled,
+                    mV2SigningEnabled,
+                    mV3SigningEnabled,
+                    mVerityEnabled,
+                    mDebuggableApkPermitted,
+                    mOtherSignersSignaturesPreserved,
+                    mCreatedBy,
+                    mSigningCertificateLineage);
+        }
+
+        /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */
+        public Builder setStampSignerConfig(SignerConfig stampSignerConfig) {
+            mStampSignerConfig = stampSignerConfig;
+            return this;
+        }
+
+        /**
+         * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+         * signing certificate rotation for certificates previously used to sign source stamps.
+         */
+        public Builder setSourceStampSigningCertificateLineage(
+                SigningCertificateLineage sourceStampSigningCertificateLineage) {
+            mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+            return this;
+        }
+
+        /**
+         * Sets whether the source stamp should contain the timestamp attribute with the time
+         * at which the source stamp was signed.
+         */
+        public Builder setSourceStampTimestampEnabled(boolean value) {
+            mSourceStampTimestampEnabled = value;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+         *
+         * <p>By default, the APK will be signed using this scheme.
+         */
+        public Builder setV1SigningEnabled(boolean enabled) {
+            mV1SigningEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
+         * scheme).
+         *
+         * <p>By default, the APK will be signed using this scheme.
+         */
+        public Builder setV2SigningEnabled(boolean enabled) {
+            mV2SigningEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature
+         * scheme).
+         *
+         * <p>By default, the APK will be signed using this scheme.
+         */
+        public Builder setV3SigningEnabled(boolean enabled) {
+            mV3SigningEnabled = enabled;
+            if (enabled) {
+                mV3SigningExplicitlyEnabled = true;
+            } else {
+                mV3SigningExplicitlyDisabled = true;
+            }
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed using the verity signature algorithm in the v2 and
+         * v3 signature blocks.
+         *
+         * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and
+         * v3 signature schemes.
+         */
+        public Builder setVerityEnabled(boolean enabled) {
+            mVerityEnabled = 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}.
+         *
+         * <p>It is dangerous to sign debuggable APKs with production/release keys because Android
+         * platform loosens security checks for such APKs. For example, arbitrary unauthorized code
+         * may be executed in the context of such an app by anybody with ADB shell access.
+         */
+        public Builder setDebuggableApkPermitted(boolean permitted) {
+            mDebuggableApkPermitted = permitted;
+            return this;
+        }
+
+        /**
+         * Sets whether signatures produced by signers other than the ones configured in this engine
+         * should be copied from the input APK to the output APK.
+         *
+         * <p>By default, signatures of other signers are omitted from the output APK.
+         */
+        public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
+            mOtherSignersSignaturesPreserved = preserved;
+            return this;
+        }
+
+        /** Sets the value of the {@code Created-By} field in JAR signature files. */
+        public Builder setCreatedBy(String createdBy) {
+            if (createdBy == null) {
+                throw new NullPointerException();
+            }
+            mCreatedBy = createdBy;
+            return this;
+        }
+
+        /**
+         * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
+         * structure provides proof of signing certificate rotation linking {@link SignerConfig}
+         * objects to previous ones.
+         */
+        public Builder setSigningCertificateLineage(
+                SigningCertificateLineage signingCertificateLineage) {
+            if (signingCertificateLineage != null) {
+                mV3SigningEnabled = true;
+                mSigningCertificateLineage = signingCertificateLineage;
+            }
+            return this;
+        }
+
+        /**
+         * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+         * key should be used to produce the APK's signature. The original signing key for the APK
+         * will be used for all previous platform versions. If a rotated key with signing lineage is
+         * not provided then this method is a noop.
+         *
+         * <p>By default, if a signing lineage is specified with {@link
+         * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme
+         * V3.1 will be used to only apply the rotation on devices running Android T+.
+         *
+         * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+         * in the original V3 signing block being used without platform targeting.
+         */
+        public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+            // If the provided SDK version does not support v3.1, then use the default SDK version
+            // with rotation support.
+            if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+            } else {
+                mRotationMinSdkVersion = minSdkVersion;
+            }
+            return this;
+        }
+
+        /**
+         * Sets whether the rotation-min-sdk-version is intended to target a development release;
+         * this is primarily required after the T SDK is finalized, and an APK needs to target U
+         * during its development cycle for rotation.
+         *
+         * <p>This is only required after the T SDK is finalized since S and earlier releases do
+         * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+         * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+         * SDK version along with setting {@code enabled} to true will allow an APK to use the
+         * rotated key on a device running U while causing this to be bypassed for T.
+         *
+         * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+         * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+         * will be a noop.
+         */
+        public Builder setRotationTargetsDevRelease(boolean enabled) {
+            mRotationTargetsDevRelease = enabled;
+            return this;
+        }
+    }
+}

+ 123 - 0
platform/android/java/editor/src/main/java/com/android/apksig/Hints.java

@@ -0,0 +1,123 @@
+/*
+ * 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;
+import java.io.IOException;
+import java.io.DataOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class Hints {
+    /**
+     * Name of hint pattern asset file in APK.
+     */
+    public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
+
+    /**
+     * Name of hint byte range data file in APK.  Keep in sync with PinnerService.java.
+     */
+    public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
+
+    private static int clampToInt(long value) {
+        return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
+    }
+
+    public static final class ByteRange {
+        final long start;
+        final long end;
+
+        public ByteRange(long start, long end) {
+            this.start = start;
+            this.end = end;
+        }
+    }
+
+    public static final class PatternWithRange {
+        final Pattern pattern;
+        final long offset;
+        final long size;
+
+        public PatternWithRange(String pattern) {
+            this.pattern = Pattern.compile(pattern);
+            this.offset= 0;
+            this.size = Long.MAX_VALUE;
+        }
+
+        public PatternWithRange(String pattern, long offset, long size) {
+            this.pattern = Pattern.compile(pattern);
+            this.offset = offset;
+            this.size = size;
+        }
+
+        public Matcher matcher(CharSequence input) {
+            return this.pattern.matcher(input);
+        }
+
+        public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
+            if (rangeIn.end - rangeIn.start < this.offset) {
+                return null;
+            }
+            long rangeOutStart = rangeIn.start + this.offset;
+            long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
+                                           this.size);
+            return new ByteRange(rangeOutStart,
+                                 rangeOutStart + rangeOutSize);
+        }
+    }
+
+    /**
+     * Create a blob of bytes that PinnerService understands as a
+     * sequence of byte ranges to pin.
+     */
+    public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
+        DataOutputStream out = new DataOutputStream(bos);
+        try {
+            for (ByteRange pinByteRange : pinByteRanges) {
+                out.writeInt(clampToInt(pinByteRange.start));
+                out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
+            }
+        } catch (IOException ex) {
+            throw new AssertionError("impossible", ex);
+        }
+        return bos.toByteArray();
+    }
+
+    public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
+        ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
+        try {
+            for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
+                String line = rawLine.replaceFirst("#.*", "");  // # starts a comment
+                String[] fields = line.split(" ");
+                if (fields.length == 1) {
+                    pinPatterns.add(new PatternWithRange(fields[0]));
+                } else if (fields.length == 3) {
+                    long start = Long.parseLong(fields[1]);
+                    long end = Long.parseLong(fields[2]);
+                    pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
+                } else {
+                    throw new AssertionError("bad pin pattern line " + line);
+                }
+            }
+        } catch (UnsupportedEncodingException ex) {
+            throw new RuntimeException("UTF-8 must be supported", ex);
+        }
+        return pinPatterns;
+    }
+}

+ 32 - 0
platform/android/java/editor/src/main/java/com/android/apksig/README.md

@@ -0,0 +1,32 @@
+# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888))
+
+apksig is a project which aims to simplify APK signing and checking whether APK signatures are
+expected to verify on Android. apksig supports
+[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File)
+(used by Android since day one) and
+[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since
+Android Nougat, API Level 24). apksig is meant to be used outside of Android devices.
+
+The key feature of apksig is that it knows about differences in APK signature verification logic
+between different versions of the Android platform. apksig thus thoroughly checks whether an APK's
+signature is expected to verify on all Android platform versions supported by the APK. When signing
+an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform
+versions supported by the APK being signed.
+
+## apksig library
+
+apksig library offers three primitives:
+
+* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions
+  supported by the APK. The range of platform versions can be customized.
+* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android
+  platform versions supported by the APK. The range of platform versions can be customized.
+* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs.
+  This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle,
+  which need to perform signing while building an APK, instead of after. For simpler use cases
+  where the APK to be signed is available upfront, the `ApkSigner` above is easier to use.
+
+_NOTE: Some public classes of the library are in packages having the word "internal" in their name.
+These are not public API of the library. Do not use \*.internal.\* classes directly because these
+classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._
+

+ 1325 - 0
platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java

@@ -0,0 +1,1325 @@
+/*
+ * 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;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.RandomAccessFileDataSink;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * APK Signer Lineage.
+ *
+ * <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
+ * the validity of its descendant.  Each additional descendant represents a new identity that can be
+ * used to sign an APK, and each generation has accompanying attributes which represent how the
+ * APK would like to view the older signing certificates, specifically how they should be trusted in
+ * certain situations.
+ *
+ * <p> Its primary use is to enable APK Signing Certificate Rotation.  The Android platform verifies
+ * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
+ * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
+ * allow upgrades to the new certificate.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class SigningCertificateLineage {
+
+    public final static int MAGIC = 0x3eff39d1;
+
+    private final static int FIRST_VERSION = 1;
+
+    private static final int CURRENT_VERSION = FIRST_VERSION;
+
+    /** accept data from already installed pkg with this cert */
+    private static final int PAST_CERT_INSTALLED_DATA = 1;
+
+    /** accept sharedUserId with pkg with this cert */
+    private static final int PAST_CERT_SHARED_USER_ID = 2;
+
+    /** grant SIGNATURE permissions to pkgs with this cert */
+    private static final int PAST_CERT_PERMISSION = 4;
+
+    /**
+     * Enable updates back to this certificate.  WARNING: this effectively removes any benefit of
+     * signing certificate changes, since a compromised key could retake control of an app even
+     * after change, and should only be used if there is a problem encountered when trying to ditch
+     * an older cert.
+     */
+    private static final int PAST_CERT_ROLLBACK = 8;
+
+    /**
+     * Preserve authenticator module-based access in AccountManager gated by signing certificate.
+     */
+    private static final int PAST_CERT_AUTH = 16;
+
+    private final int mMinSdkVersion;
+
+    /**
+     * The signing lineage is just a list of nodes, with the first being the original signing
+     * certificate and the most recent being the one with which the APK is to actually be signed.
+     */
+    private final List<SigningCertificateNode> mSigningLineage;
+
+    private SigningCertificateLineage(int minSdkVersion, List<SigningCertificateNode> list) {
+        mMinSdkVersion = minSdkVersion;
+        mSigningLineage = list;
+    }
+
+    /**
+     * Creates a {@code SigningCertificateLineage} with a single signer in the lineage.
+     */
+    private static SigningCertificateLineage createSigningLineage(int minSdkVersion,
+            SignerConfig signer, SignerCapabilities capabilities) {
+        SigningCertificateLineage signingCertificateLineage = new SigningCertificateLineage(
+                minSdkVersion, new ArrayList<>());
+        return signingCertificateLineage.spawnFirstDescendant(signer, capabilities);
+    }
+
+    private static SigningCertificateLineage createSigningLineage(
+            int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities,
+            SignerConfig child, SignerCapabilities childCapabilities)
+            throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+            SignatureException {
+        SigningCertificateLineage signingCertificateLineage =
+                new SigningCertificateLineage(minSdkVersion, new ArrayList<>());
+        signingCertificateLineage =
+                signingCertificateLineage.spawnFirstDescendant(parent, parentCapabilities);
+        return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities);
+    }
+
+    public static SigningCertificateLineage readFromBytes(byte[] lineageBytes)
+            throws IOException {
+        return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes)));
+    }
+
+    public static SigningCertificateLineage readFromFile(File file)
+            throws IOException {
+        if (file == null) {
+            throw new NullPointerException("file == null");
+        }
+        RandomAccessFile inputFile = new RandomAccessFile(file, "r");
+        return readFromDataSource(DataSources.asDataSource(inputFile));
+    }
+
+    public static SigningCertificateLineage readFromDataSource(DataSource dataSource)
+            throws IOException {
+        if (dataSource == null) {
+            throw new NullPointerException("dataSource == null");
+        }
+        ByteBuffer inBuff = dataSource.getByteBuffer(0, (int) dataSource.size());
+        inBuff.order(ByteOrder.LITTLE_ENDIAN);
+        return read(inBuff);
+    }
+
+    /**
+     * Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute.
+     *
+     * <note>
+     *     this may not give a complete representation of an APK's signing certificate history,
+     *     since the APK may have multiple signers corresponding to different platform versions.
+     *     Use <code> readFromApkFile</code> to handle this case.
+     * </note>
+     * @param attrValue
+     */
+    public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue)
+            throws IOException {
+        List<SigningCertificateNode> parsedLineage =
+                V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap(
+                        attrValue).order(ByteOrder.LITTLE_ENDIAN));
+        int minSdkVersion = calculateMinSdkVersion(parsedLineage);
+        return  new SigningCertificateLineage(minSdkVersion, parsedLineage);
+    }
+
+    /**
+     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
+     * signature block of the provided APK File.
+     *
+     * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
+     * or if the V3 signature block does not contain a valid lineage.
+     */
+    public static SigningCertificateLineage readFromApkFile(File apkFile)
+            throws IOException, ApkFormatException {
+        try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) {
+            DataSource apk = DataSources.asDataSource(f, 0, f.length());
+            return readFromApkDataSource(apk);
+        }
+    }
+
+    /**
+     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 and
+     * V3.1 signature blocks of the provided APK DataSource.
+     *
+     * @throws IllegalArgumentException if the provided APK does not contain a V3 nor V3.1
+     * signature block, or if the V3 and V3.1 signature blocks do not contain a valid lineage.
+     */
+
+    public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
+            throws IOException, ApkFormatException {
+        return readFromApkDataSource(apk, /* readV31Lineage= */ true,  /* readV3Lineage= */true);
+    }
+
+    /**
+     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3.1
+     * signature blocks of the provided APK DataSource.
+     *
+     * @throws IllegalArgumentException if the provided APK does not contain a V3.1 signature block,
+     * or if the V3.1 signature block does not contain a valid lineage.
+     */
+
+    public static SigningCertificateLineage readV31FromApkDataSource(DataSource apk)
+            throws IOException, ApkFormatException {
+            return readFromApkDataSource(apk, /* readV31Lineage= */ true,
+                        /* readV3Lineage= */ false);
+    }
+
+    private static SigningCertificateLineage readFromApkDataSource(
+            DataSource apk,
+            boolean readV31Lineage,
+            boolean readV3Lineage)
+            throws IOException, ApkFormatException {
+        ApkUtils.ZipSections zipSections;
+        try {
+            zipSections = ApkUtils.findZipSections(apk);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException(e.getMessage());
+        }
+
+        List<SignatureInfo> signatureInfoList = new ArrayList<>();
+        if (readV31Lineage) {
+            try {
+                ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                    ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+                signatureInfoList.add(
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                        V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+            } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                // This could be expected if there's only a V3 signature block.
+            }
+        }
+        if (readV3Lineage) {
+            try {
+                ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                    ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+                signatureInfoList.add(
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                        V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result));
+            } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                // This could be expected if the provided APK is not signed with the V3 signature
+                // scheme
+            }
+        }
+        if (signatureInfoList.isEmpty()) {
+            String message;
+            if (readV31Lineage && readV3Lineage) {
+                message = "The provided APK does not contain a valid V3 nor V3.1 signature block.";
+            } else if (readV31Lineage) {
+                message = "The provided APK does not contain a valid V3.1 signature block.";
+            } else if (readV3Lineage) {
+                message = "The provided APK does not contain a valid V3 signature block.";
+            } else {
+                message = "No signature blocks were requested.";
+            }
+            throw new IllegalArgumentException(message);
+        }
+
+        List<SigningCertificateLineage> lineages = new ArrayList<>(1);
+        for (SignatureInfo signatureInfo : signatureInfoList) {
+            // FORMAT:
+            // * length-prefixed sequence of length-prefixed signers:
+            //   * length-prefixed signed data
+            //   * minSDK
+            //   * maxSDK
+            //   * length-prefixed sequence of length-prefixed signatures
+            //   * length-prefixed public key
+            ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+            while (signers.hasRemaining()) {
+                ByteBuffer signer = getLengthPrefixedSlice(signers);
+                ByteBuffer signedData = getLengthPrefixedSlice(signer);
+                try {
+                    SigningCertificateLineage lineage = readFromSignedData(signedData);
+                    lineages.add(lineage);
+                } catch (IllegalArgumentException ignored) {
+                    // The current signer block does not contain a valid lineage, but it is possible
+                    // another block will.
+                }
+            }
+        }
+
+        SigningCertificateLineage result;
+        if (lineages.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "The provided APK does not contain a valid lineage.");
+        } else if (lineages.size() > 1) {
+            result = consolidateLineages(lineages);
+        } else {
+            result = lineages.get(0);
+        }
+        return result;
+    }
+
+    /**
+     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the provided
+     * signed data portion of a signer in a V3 signature block.
+     *
+     * @throws IllegalArgumentException if the provided signed data does not contain a valid
+     * lineage.
+     */
+    public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData)
+            throws IOException, ApkFormatException {
+        // FORMAT:
+        //   * length-prefixed sequence of length-prefixed digests:
+        //   * length-prefixed sequence of certificates:
+        //     * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+        //   * uint-32: minSdkVersion
+        //   * uint-32: maxSdkVersion
+        //   * length-prefixed sequence of length-prefixed additional attributes:
+        //     * uint32: ID
+        //     * (length - 4) bytes: value
+        //     * uint32: Proof-of-rotation ID: 0x3ba06f8c
+        //     * length-prefixed proof-of-rotation structure
+        // consume the digests through the maxSdkVersion to reach the lineage in the attributes
+        getLengthPrefixedSlice(signedData);
+        getLengthPrefixedSlice(signedData);
+        signedData.getInt();
+        signedData.getInt();
+        // iterate over the additional attributes adding any lineages to the List
+        ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+        List<SigningCertificateLineage> lineages = new ArrayList<>(1);
+        while (additionalAttributes.hasRemaining()) {
+            ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
+            int id = attribute.getInt();
+            if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
+                byte[] value = ByteBufferUtils.toByteArray(attribute);
+                SigningCertificateLineage lineage = readFromV3AttributeValue(value);
+                lineages.add(lineage);
+            }
+        }
+        SigningCertificateLineage result;
+        // There should only be a single attribute with the lineage, but if there are multiple then
+        // attempt to consolidate the lineages.
+        if (lineages.isEmpty()) {
+            throw new IllegalArgumentException("The signed data does not contain a valid lineage.");
+        } else if (lineages.size() > 1) {
+            result = consolidateLineages(lineages);
+        } else {
+            result = lineages.get(0);
+        }
+        return result;
+    }
+
+    public byte[] getBytes() {
+        return write().array();
+    }
+
+    public void writeToFile(File file) throws IOException {
+        if (file == null) {
+            throw new NullPointerException("file == null");
+        }
+        RandomAccessFile outputFile = new RandomAccessFile(file, "rw");
+        writeToDataSink(new RandomAccessFileDataSink(outputFile));
+    }
+
+    public void writeToDataSink(DataSink dataSink) throws IOException {
+        if (dataSink == null) {
+            throw new NullPointerException("dataSink == null");
+        }
+        dataSink.consume(write());
+    }
+
+    /**
+     * Add a new signing certificate to the lineage.  This effectively creates a signing certificate
+     * rotation event, forcing APKs which include this lineage to be signed by the new signer. The
+     * flags associated with the new signer are set to a default value.
+     *
+     * @param parent current signing certificate of the containing APK
+     * @param child new signing certificate which will sign the APK contents
+     */
+    public SigningCertificateLineage spawnDescendant(SignerConfig parent, SignerConfig child)
+            throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+            SignatureException {
+        if (parent == null || child == null) {
+            throw new NullPointerException("can't add new descendant to lineage with null inputs");
+        }
+        SignerCapabilities signerCapabilities = new SignerCapabilities.Builder().build();
+        return spawnDescendant(parent, child, signerCapabilities);
+    }
+
+    /**
+     * Add a new signing certificate to the lineage.  This effectively creates a signing certificate
+     * rotation event, forcing APKs which include this lineage to be signed by the new signer.
+     *
+     * @param parent current signing certificate of the containing APK
+     * @param child new signing certificate which will sign the APK contents
+     * @param childCapabilities flags
+     */
+    public SigningCertificateLineage spawnDescendant(
+            SignerConfig parent, SignerConfig child, SignerCapabilities childCapabilities)
+            throws CertificateEncodingException, InvalidKeyException,
+            NoSuchAlgorithmException, SignatureException {
+        if (parent == null) {
+            throw new NullPointerException("parent == null");
+        }
+        if (child == null) {
+            throw new NullPointerException("child == null");
+        }
+        if (childCapabilities == null) {
+            throw new NullPointerException("childCapabilities == null");
+        }
+        if (mSigningLineage.isEmpty()) {
+            throw new IllegalArgumentException("Cannot spawn descendant signing certificate on an"
+                    + " empty SigningCertificateLineage: no parent node");
+        }
+
+        // make sure that the parent matches our newest generation (leaf node/sink)
+        SigningCertificateNode currentGeneration = mSigningLineage.get(mSigningLineage.size() - 1);
+        if (!Arrays.equals(currentGeneration.signingCert.getEncoded(),
+                parent.getCertificate().getEncoded())) {
+            throw new IllegalArgumentException("SignerConfig Certificate containing private key"
+                    + " to sign the new SigningCertificateLineage record does not match the"
+                    + " existing most recent record");
+        }
+
+        // create data to be signed, including the algorithm we're going to use
+        SignatureAlgorithm signatureAlgorithm = getSignatureAlgorithm(parent);
+        ByteBuffer prefixedSignedData = ByteBuffer.wrap(
+                V3SigningCertificateLineage.encodeSignedData(
+                        child.getCertificate(), signatureAlgorithm.getId()));
+        prefixedSignedData.position(4);
+        ByteBuffer signedDataBuffer = ByteBuffer.allocate(prefixedSignedData.remaining());
+        signedDataBuffer.put(prefixedSignedData);
+        byte[] signedData = signedDataBuffer.array();
+
+        // create SignerConfig to do the signing
+        List<X509Certificate> certificates = new ArrayList<>(1);
+        certificates.add(parent.getCertificate());
+        ApkSigningBlockUtils.SignerConfig newSignerConfig =
+                new ApkSigningBlockUtils.SignerConfig();
+        newSignerConfig.privateKey = parent.getPrivateKey();
+        newSignerConfig.certificates = certificates;
+        newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm);
+
+        // sign it
+        List<Pair<Integer, byte[]>> signatures =
+                ApkSigningBlockUtils.generateSignaturesOverData(newSignerConfig, signedData);
+
+        // finally, add it to our lineage
+        SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(signatures.get(0).getFirst());
+        byte[] signature = signatures.get(0).getSecond();
+        currentGeneration.sigAlgorithm = sigAlgorithm;
+        SigningCertificateNode childNode =
+                new SigningCertificateNode(
+                        child.getCertificate(), sigAlgorithm, null,
+                        signature, childCapabilities.getFlags());
+        List<SigningCertificateNode> lineageCopy = new ArrayList<>(mSigningLineage);
+        lineageCopy.add(childNode);
+        return new SigningCertificateLineage(mMinSdkVersion, lineageCopy);
+    }
+
+    /**
+     * The number of signing certificates in the lineage, including the current signer, which means
+     * this value can also be used to V2determine the number of signing certificate rotations by
+     * subtracting 1.
+     */
+    public int size() {
+        return mSigningLineage.size();
+    }
+
+    private SignatureAlgorithm getSignatureAlgorithm(SignerConfig parent)
+            throws InvalidKeyException {
+        PublicKey publicKey = parent.getCertificate().getPublicKey();
+
+        // TODO switch to one signature algorithm selection, or add support for multiple algorithms
+        List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+                publicKey, mMinSdkVersion, false /* verityEnabled */,
+                false /* deterministicDsaSigning */);
+        return algorithms.get(0);
+    }
+
+    private SigningCertificateLineage spawnFirstDescendant(
+            SignerConfig parent, SignerCapabilities signerCapabilities) {
+        if (!mSigningLineage.isEmpty()) {
+            throw new IllegalStateException("SigningCertificateLineage already has its first node");
+        }
+
+        // check to make sure that the public key for the first node is acceptable for our minSdk
+        try {
+            getSignatureAlgorithm(parent);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("Algorithm associated with first signing certificate"
+                    + " invalid on desired platform versions", e);
+        }
+
+        // create "fake" signed data (there will be no signature over it, since there is no parent
+        SigningCertificateNode firstNode = new SigningCertificateNode(
+                parent.getCertificate(), null, null, new byte[0], signerCapabilities.getFlags());
+        return new SigningCertificateLineage(mMinSdkVersion, Collections.singletonList(firstNode));
+    }
+
+    private static SigningCertificateLineage read(ByteBuffer inputByteBuffer)
+            throws IOException {
+        ApkSigningBlockUtils.checkByteOrderLittleEndian(inputByteBuffer);
+        if (inputByteBuffer.remaining() < 8) {
+            throw new IllegalArgumentException(
+                    "Improper SigningCertificateLineage format: insufficient data for header.");
+        }
+
+        if (inputByteBuffer.getInt() != MAGIC) {
+            throw new IllegalArgumentException(
+                    "Improper SigningCertificateLineage format: MAGIC header mismatch.");
+        }
+        return read(inputByteBuffer, inputByteBuffer.getInt());
+    }
+
+    private static SigningCertificateLineage read(ByteBuffer inputByteBuffer, int version)
+            throws IOException {
+        switch (version) {
+            case FIRST_VERSION:
+                try {
+                    List<SigningCertificateNode> nodes =
+                            V3SigningCertificateLineage.readSigningCertificateLineage(
+                                    getLengthPrefixedSlice(inputByteBuffer));
+                    int minSdkVersion = calculateMinSdkVersion(nodes);
+                    return new SigningCertificateLineage(minSdkVersion, nodes);
+                } catch (ApkFormatException e) {
+                    // unable to get a proper length-prefixed lineage slice
+                    throw new IOException("Unable to read list of signing certificate nodes in "
+                            + "SigningCertificateLineage", e);
+                }
+            default:
+                throw new IllegalArgumentException(
+                        "Improper SigningCertificateLineage format: unrecognized version.");
+        }
+    }
+
+    private static int calculateMinSdkVersion(List<SigningCertificateNode> nodes) {
+        if (nodes == null) {
+            throw new IllegalArgumentException("Can't calculate minimum SDK version of null nodes");
+        }
+        int minSdkVersion = AndroidSdkVersion.P; // lineage introduced in P
+        for (SigningCertificateNode node : nodes) {
+            if (node.sigAlgorithm != null) {
+                int nodeMinSdkVersion = node.sigAlgorithm.getMinSdkVersion();
+                if (nodeMinSdkVersion > minSdkVersion) {
+                    minSdkVersion = nodeMinSdkVersion;
+                }
+            }
+        }
+        return minSdkVersion;
+    }
+
+    private ByteBuffer write() {
+        byte[] encodedLineage =
+                V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+        int payloadSize = 4 + 4 + 4 + encodedLineage.length;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(MAGIC);
+        result.putInt(CURRENT_VERSION);
+        result.putInt(encodedLineage.length);
+        result.put(encodedLineage);
+        result.flip();
+        return result;
+    }
+
+    public byte[] encodeSigningCertificateLineage() {
+        return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+    }
+
+    public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs(
+            List<DefaultApkSignerEngine.SignerConfig> signerConfigs) {
+        if (signerConfigs == null) {
+            throw new NullPointerException("signerConfigs == null");
+        }
+
+        // not the most elegant sort, but we expect signerConfigs to be quite small (1 or 2 signers
+        // in most cases) and likely already sorted, so not worth the overhead of doing anything
+        // fancier
+        List<DefaultApkSignerEngine.SignerConfig> sortedSignerConfigs =
+                new ArrayList<>(signerConfigs.size());
+        for (int i = 0; i < mSigningLineage.size(); i++) {
+            for (int j = 0; j < signerConfigs.size(); j++) {
+                DefaultApkSignerEngine.SignerConfig config = signerConfigs.get(j);
+                if (mSigningLineage.get(i).signingCert.equals(config.getCertificates().get(0))) {
+                    sortedSignerConfigs.add(config);
+                    break;
+                }
+            }
+        }
+        if (sortedSignerConfigs.size() != signerConfigs.size()) {
+            throw new IllegalArgumentException("SignerConfigs supplied which are not present in the"
+                    + " SigningCertificateLineage");
+        }
+        return sortedSignerConfigs;
+    }
+
+    /**
+     * Returns the SignerCapabilities for the signer in the lineage that matches the provided
+     * config.
+     */
+    public SignerCapabilities getSignerCapabilities(SignerConfig config) {
+        if (config == null) {
+            throw new NullPointerException("config == null");
+        }
+
+        X509Certificate cert = config.getCertificate();
+        return getSignerCapabilities(cert);
+    }
+
+    /**
+     * Returns the SignerCapabilities for the signer in the lineage that matches the provided
+     * certificate.
+     */
+    public SignerCapabilities getSignerCapabilities(X509Certificate cert) {
+        if (cert == null) {
+            throw new NullPointerException("cert == null");
+        }
+
+        for (int i = 0; i < mSigningLineage.size(); i++) {
+            SigningCertificateNode lineageNode = mSigningLineage.get(i);
+            if (lineageNode.signingCert.equals(cert)) {
+                int flags = lineageNode.flags;
+                return new SignerCapabilities.Builder(flags).build();
+            }
+        }
+
+        // the provided signer certificate was not found in the lineage
+        throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
+                + ") not found in the SigningCertificateLineage");
+    }
+
+    /**
+     * Updates the SignerCapabilities for the signer in the lineage that matches the provided
+     * config. Only those capabilities that have been modified through the setXX methods will be
+     * updated for the signer to prevent unset default values from being applied.
+     */
+    public void updateSignerCapabilities(SignerConfig config, SignerCapabilities capabilities) {
+        if (config == null) {
+            throw new NullPointerException("config == null");
+        }
+        updateSignerCapabilities(config.getCertificate(), capabilities);
+    }
+
+    /**
+     * Updates the {@code capabilities} for the signer with the provided {@code certificate} in the
+     * lineage. Only those capabilities that have been modified through the setXX methods will be
+     * updated for the signer to prevent unset default values from being applied.
+     */
+    public void updateSignerCapabilities(X509Certificate certificate,
+            SignerCapabilities capabilities) {
+        if (certificate == null) {
+            throw new NullPointerException("config == null");
+        }
+
+        for (int i = 0; i < mSigningLineage.size(); i++) {
+            SigningCertificateNode lineageNode = mSigningLineage.get(i);
+            if (lineageNode.signingCert.equals(certificate)) {
+                int flags = lineageNode.flags;
+                SignerCapabilities newCapabilities = new SignerCapabilities.Builder(
+                        flags).setCallerConfiguredCapabilities(capabilities).build();
+                lineageNode.flags = newCapabilities.getFlags();
+                return;
+            }
+        }
+
+        // the provided signer config was not found in the lineage
+        throw new IllegalArgumentException("Certificate (" + certificate.getSubjectDN()
+                + ") not found in the SigningCertificateLineage");
+    }
+
+    /**
+     * Returns a list containing all of the certificates in the lineage.
+     */
+    public List<X509Certificate> getCertificatesInLineage() {
+        List<X509Certificate> certs = new ArrayList<>();
+        for (int i = 0; i < mSigningLineage.size(); i++) {
+            X509Certificate cert = mSigningLineage.get(i).signingCert;
+            certs.add(cert);
+        }
+        return certs;
+    }
+
+    /**
+     * Returns {@code true} if the specified config is in the lineage.
+     */
+    public boolean isSignerInLineage(SignerConfig config) {
+        if (config == null) {
+            throw new NullPointerException("config == null");
+        }
+
+        X509Certificate cert = config.getCertificate();
+        return isCertificateInLineage(cert);
+    }
+
+    /**
+     * Returns {@code true} if the specified certificate is in the lineage.
+     */
+    public boolean isCertificateInLineage(X509Certificate cert) {
+        if (cert == null) {
+            throw new NullPointerException("cert == null");
+        }
+
+        for (int i = 0; i < mSigningLineage.size(); i++) {
+            if (mSigningLineage.get(i).signingCert.equals(cert)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether the provided {@code cert} is the latest signing certificate in the lineage.
+     *
+     * <p>This method will only compare the provided {@code cert} against the latest signing
+     * certificate in the lineage; if a certificate that is not in the lineage is provided, this
+     * method will return false.
+     */
+    public boolean isCertificateLatestInLineage(X509Certificate cert) {
+        if (cert == null) {
+            throw new NullPointerException("cert == null");
+        }
+
+        return mSigningLineage.get(mSigningLineage.size() - 1).signingCert.equals(cert);
+    }
+
+    private static int calculateDefaultFlags() {
+        return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION
+                | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH;
+    }
+
+    /**
+     * Returns a new SigningCertificateLineage which terminates at the node corresponding to the
+     * given certificate.  This is useful in the event of rotating to a new signing algorithm that
+     * is only supported on some platform versions.  It enables a v3 signature to be generated using
+     * this signing certificate and the shortened proof-of-rotation record from this sub lineage in
+     * conjunction with the appropriate SDK version values.
+     *
+     * @param x509Certificate the signing certificate for which to search
+     * @return A new SigningCertificateLineage if the given certificate is present.
+     *
+     * @throws IllegalArgumentException if the provided certificate is not in the lineage.
+     */
+    public SigningCertificateLineage getSubLineage(X509Certificate x509Certificate) {
+        if (x509Certificate == null) {
+            throw new NullPointerException("x509Certificate == null");
+        }
+        for (int i = 0; i < mSigningLineage.size(); i++) {
+            if (mSigningLineage.get(i).signingCert.equals(x509Certificate)) {
+                return new SigningCertificateLineage(
+                        mMinSdkVersion, new ArrayList<>(mSigningLineage.subList(0, i + 1)));
+            }
+        }
+
+        // looks like we didn't find the cert,
+        throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage");
+    }
+
+    /**
+     * Consolidates all of the lineages found in an APK into one lineage. In so doing, it also
+     * checks that all of the lineages are contained in one common lineage.
+     *
+     * An APK may contain multiple lineages, one for each signer, which correspond to different
+     * supported platform versions.  In this event, the lineage(s) from the earlier platform
+     * version(s) should be present in the most recent, either directly or via a sublineage
+     * that would allow the earlier lineages to merge with the most recent.
+     *
+     * <note> This does not verify that the largest lineage corresponds to the most recent supported
+     * platform version.  That check is performed during v3 verification. </note>
+     */
+    public static SigningCertificateLineage consolidateLineages(
+            List<SigningCertificateLineage> lineages) {
+        if (lineages == null || lineages.isEmpty()) {
+            return null;
+        }
+        SigningCertificateLineage consolidatedLineage = lineages.get(0);
+        for (int i = 1; i < lineages.size(); i++) {
+            consolidatedLineage = consolidatedLineage.mergeLineageWith(lineages.get(i));
+        }
+        return consolidatedLineage;
+    }
+
+    /**
+     * Merges this lineage with the provided {@code otherLineage}.
+     *
+     * <p>The merged lineage does not currently handle merging capabilities of common signers and
+     * should only be used to determine the full signing history of a collection of lineages.
+     */
+    public SigningCertificateLineage mergeLineageWith(SigningCertificateLineage otherLineage) {
+        // Determine the ancestor and descendant lineages; if the original signer is in the other
+        // lineage, then it is considered a descendant.
+        SigningCertificateLineage ancestorLineage;
+        SigningCertificateLineage descendantLineage;
+        X509Certificate signerCert = mSigningLineage.get(0).signingCert;
+        if (otherLineage.isCertificateInLineage(signerCert)) {
+            descendantLineage = this;
+            ancestorLineage = otherLineage;
+        } else {
+            descendantLineage = otherLineage;
+            ancestorLineage = this;
+        }
+
+        int ancestorIndex = 0;
+        int descendantIndex = 0;
+        SigningCertificateNode ancestorNode;
+        SigningCertificateNode descendantNode = descendantLineage.mSigningLineage.get(
+                descendantIndex++);
+        List<SigningCertificateNode> mergedLineage = new ArrayList<>();
+        // Iterate through the ancestor lineage and add the current node to the resulting lineage
+        // until the first node of the descendant is found.
+        while (ancestorIndex < ancestorLineage.size()) {
+            ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++);
+            if (ancestorNode.signingCert.equals(descendantNode.signingCert)) {
+                break;
+            }
+            mergedLineage.add(ancestorNode);
+        }
+        // If all of the nodes in the ancestor lineage have been added to the merged lineage, then
+        // there is no overlap between this and the provided lineage.
+        if (ancestorIndex == mergedLineage.size()) {
+            throw new IllegalArgumentException(
+                    "The provided lineage is not a descendant or an ancestor of this lineage");
+        }
+        // The descendant lineage's first node was in the ancestor's lineage above; add it to the
+        // merged lineage.
+        mergedLineage.add(descendantNode);
+        while (ancestorIndex < ancestorLineage.size()
+                && descendantIndex < descendantLineage.size()) {
+            ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++);
+            descendantNode = descendantLineage.mSigningLineage.get(descendantIndex++);
+            if (!ancestorNode.signingCert.equals(descendantNode.signingCert)) {
+                throw new IllegalArgumentException(
+                        "The provided lineage diverges from this lineage");
+            }
+            mergedLineage.add(descendantNode);
+        }
+        // At this point, one or both of the lineages have been exhausted and all signers to this
+        // point were a match between the two lineages; add any remaining elements from either
+        // lineage to the merged lineage.
+        while (ancestorIndex < ancestorLineage.size()) {
+            mergedLineage.add(ancestorLineage.mSigningLineage.get(ancestorIndex++));
+        }
+        while (descendantIndex < descendantLineage.size()) {
+            mergedLineage.add(descendantLineage.mSigningLineage.get(descendantIndex++));
+        }
+        return new SigningCertificateLineage(Math.min(mMinSdkVersion, otherLineage.mMinSdkVersion),
+                mergedLineage);
+    }
+
+    /**
+     * Checks whether given lineages are compatible. Returns {@code true} if an installed APK with
+     * the oldLineage could be updated with an APK with the newLineage.
+     */
+    public static boolean checkLineagesCompatibility(
+        SigningCertificateLineage oldLineage, SigningCertificateLineage newLineage) {
+
+        final ArrayList<X509Certificate> oldCertificates = oldLineage == null ?
+                new ArrayList<X509Certificate>()
+                : new ArrayList(oldLineage.getCertificatesInLineage());
+        final ArrayList<X509Certificate> newCertificates = newLineage == null ?
+                new ArrayList<X509Certificate>()
+                : new ArrayList(newLineage.getCertificatesInLineage());
+
+        if (oldCertificates.isEmpty()) {
+            return true;
+        }
+        if (newCertificates.isEmpty()) {
+            return false;
+        }
+
+        // Both lineages contain exactly the same certificates or the new lineage extends
+        // the old one. The capabilities of particular certificates may have changed though but it
+        // does not matter in terms of current compatibility.
+        if (newCertificates.size() >= oldCertificates.size()
+                && newCertificates.subList(0, oldCertificates.size()).equals(oldCertificates)) {
+            return true;
+        }
+
+        ArrayList<X509Certificate> newCertificatesArray = new ArrayList(newCertificates);
+        ArrayList<X509Certificate> oldCertificatesArray = new ArrayList(oldCertificates);
+
+        int lastOldCertIndexInNew = newCertificatesArray.lastIndexOf(
+                    oldCertificatesArray.get(oldCertificatesArray.size()-1));
+
+        // The new lineage trims some nodes from the beginning of the old lineage and possibly
+        // extends it at the end. The new lineage must contain the old signing certificate and
+        // the nodes up until the node with signing certificate must be in the same order.
+        // Good example 1:
+        //    old: A -> B -> C
+        //    new: B -> C -> D
+        // Good example 2:
+        //    old: A -> B -> C
+        //    new: C
+        // Bad example 1:
+        //    old: A -> B -> C
+        //    new: A -> C
+        // Bad example 1:
+        //    old: A -> B
+        //    new: C -> B
+        if (lastOldCertIndexInNew >= 0) {
+            return newCertificatesArray.subList(0, lastOldCertIndexInNew+1).equals(
+                    oldCertificatesArray.subList(
+                            oldCertificates.size()-1-lastOldCertIndexInNew,
+                            oldCertificatesArray.size()));
+        }
+
+
+        // The new lineage can be shorter than the old one only if the last certificate of the new
+        // lineage exists in the old lineage and has a rollback capability there.
+        // Good example:
+        //    old: A -> B_withRollbackCapability -> C
+        //    new: A -> B
+        // Bad example 1:
+        //    old: A -> B -> C
+        //    new: A -> B
+        // Bad example 2:
+        //    old: A -> B_withRollbackCapability -> C
+        //    new: A -> B -> D
+        return  oldCertificates.subList(0, newCertificates.size()).equals(newCertificates)
+                && oldLineage.getSignerCapabilities(
+                        oldCertificates.get(newCertificates.size()-1)).hasRollback();
+    }
+
+    /**
+     * Representation of the capabilities the APK would like to grant to its old signing
+     * certificates.  The {@code SigningCertificateLineage} provides two conceptual data structures.
+     *   1) proof of rotation - Evidence that other parties can trust an APK's current signing
+     *      certificate if they trust an older one in this lineage
+     *   2) self-trust - certain capabilities may have been granted by an APK to other parties based
+     *      on its own signing certificate.  When it changes its signing certificate it may want to
+     *      allow the other parties to retain those capabilities.
+     * {@code SignerCapabilties} provides a representation of the second structure.
+     *
+     * <p>Use {@link Builder} to obtain configuration instances.
+     */
+    public static class SignerCapabilities {
+        private final int mFlags;
+
+        private final int mCallerConfiguredFlags;
+
+        private SignerCapabilities(int flags) {
+            this(flags, 0);
+        }
+
+        private SignerCapabilities(int flags, int callerConfiguredFlags) {
+            mFlags = flags;
+            mCallerConfiguredFlags = callerConfiguredFlags;
+        }
+
+        private int getFlags() {
+            return mFlags;
+        }
+
+        /**
+         * Returns {@code true} if the capabilities of this object match those of the provided
+         * object.
+         */
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) return true;
+            if (!(other instanceof SignerCapabilities)) return false;
+
+            return this.mFlags == ((SignerCapabilities) other).mFlags;
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * mFlags;
+        }
+
+        /**
+         * Returns {@code true} if this object has the installed data capability.
+         */
+        public boolean hasInstalledData() {
+            return (mFlags & PAST_CERT_INSTALLED_DATA) != 0;
+        }
+
+        /**
+         * Returns {@code true} if this object has the shared UID capability.
+         */
+        public boolean hasSharedUid() {
+            return (mFlags & PAST_CERT_SHARED_USER_ID) != 0;
+        }
+
+        /**
+         * Returns {@code true} if this object has the permission capability.
+         */
+        public boolean hasPermission() {
+            return (mFlags & PAST_CERT_PERMISSION) != 0;
+        }
+
+        /**
+         * Returns {@code true} if this object has the rollback capability.
+         */
+        public boolean hasRollback() {
+            return (mFlags & PAST_CERT_ROLLBACK) != 0;
+        }
+
+        /**
+         * Returns {@code true} if this object has the auth capability.
+         */
+        public boolean hasAuth() {
+            return (mFlags & PAST_CERT_AUTH) != 0;
+        }
+
+        /**
+         * Builder of {@link SignerCapabilities} instances.
+         */
+        public static class Builder {
+            private int mFlags;
+
+            private int mCallerConfiguredFlags;
+
+            /**
+             * Constructs a new {@code Builder}.
+             */
+            public Builder() {
+                mFlags = calculateDefaultFlags();
+            }
+
+            /**
+             * Constructs a new {@code Builder} with the initial capabilities set to the provided
+             * flags.
+             */
+            public Builder(int flags) {
+                mFlags = flags;
+            }
+
+            /**
+             * Set the {@code PAST_CERT_INSTALLED_DATA} flag in this capabilities object.  This flag
+             * is used by the platform to determine if installed data associated with previous
+             * signing certificate should be trusted.  In particular, this capability is required to
+             * perform signing certificate rotation during an upgrade on-device.  Without it, the
+             * platform will not permit the app data from the old signing certificate to
+             * propagate to the new version.  Typically, this flag should be set to enable signing
+             * certificate rotation, and may be unset later when the app developer is satisfied that
+             * their install base is as migrated as it will be.
+             */
+            public Builder setInstalledData(boolean enabled) {
+                mCallerConfiguredFlags |= PAST_CERT_INSTALLED_DATA;
+                if (enabled) {
+                    mFlags |= PAST_CERT_INSTALLED_DATA;
+                } else {
+                    mFlags &= ~PAST_CERT_INSTALLED_DATA;
+                }
+                return this;
+            }
+
+            /**
+             * Set the {@code PAST_CERT_SHARED_USER_ID} flag in this capabilities object.  This flag
+             * is used by the platform to determine if this app is willing to be sharedUid with
+             * other apps which are still signed with the associated signing certificate.  This is
+             * useful in situations where sharedUserId apps would like to change their signing
+             * certificate, but can't guarantee the order of updates to those apps.
+             */
+            public Builder setSharedUid(boolean enabled) {
+                mCallerConfiguredFlags |= PAST_CERT_SHARED_USER_ID;
+                if (enabled) {
+                    mFlags |= PAST_CERT_SHARED_USER_ID;
+                } else {
+                    mFlags &= ~PAST_CERT_SHARED_USER_ID;
+                }
+                return this;
+            }
+
+            /**
+             * Set the {@code PAST_CERT_PERMISSION} flag in this capabilities object.  This flag
+             * is used by the platform to determine if this app is willing to grant SIGNATURE
+             * permissions to apps signed with the associated signing certificate.  Without this
+             * capability, an application signed with the older certificate will not be granted the
+             * SIGNATURE permissions defined by this app.  In addition, if multiple apps define the
+             * same SIGNATURE permission, the second one the platform sees will not be installable
+             * if this capability is not set and the signing certificates differ.
+             */
+            public Builder setPermission(boolean enabled) {
+                mCallerConfiguredFlags |= PAST_CERT_PERMISSION;
+                if (enabled) {
+                    mFlags |= PAST_CERT_PERMISSION;
+                } else {
+                    mFlags &= ~PAST_CERT_PERMISSION;
+                }
+                return this;
+            }
+
+            /**
+             * Set the {@code PAST_CERT_ROLLBACK} flag in this capabilities object.  This flag
+             * is used by the platform to determine if this app is willing to upgrade to a new
+             * version that is signed by one of its past signing certificates.
+             *
+             * <note> WARNING: this effectively removes any benefit of signing certificate changes,
+             * since a compromised key could retake control of an app even after change, and should
+             * only be used if there is a problem encountered when trying to ditch an older cert
+             * </note>
+             */
+            public Builder setRollback(boolean enabled) {
+                mCallerConfiguredFlags |= PAST_CERT_ROLLBACK;
+                if (enabled) {
+                    mFlags |= PAST_CERT_ROLLBACK;
+                } else {
+                    mFlags &= ~PAST_CERT_ROLLBACK;
+                }
+                return this;
+            }
+
+            /**
+             * Set the {@code PAST_CERT_AUTH} flag in this capabilities object.  This flag
+             * is used by the platform to determine whether or not privileged access based on
+             * authenticator module signing certificates should be granted.
+             */
+            public Builder setAuth(boolean enabled) {
+                mCallerConfiguredFlags |= PAST_CERT_AUTH;
+                if (enabled) {
+                    mFlags |= PAST_CERT_AUTH;
+                } else {
+                    mFlags &= ~PAST_CERT_AUTH;
+                }
+                return this;
+            }
+
+            /**
+             * Applies the capabilities that were explicitly set in the provided capabilities object
+             * to this builder. Any values that were not set will not be applied to this builder
+             * to prevent unintentinoally setting a capability back to a default value.
+             */
+            public Builder setCallerConfiguredCapabilities(SignerCapabilities capabilities) {
+                // The mCallerConfiguredFlags should have a bit set for each capability that was
+                // set by a caller. If a capability was explicitly set then the corresponding bit
+                // in mCallerConfiguredFlags should be set. This allows the provided capabilities
+                // to take effect for those set by the caller while those that were not set will
+                // be cleared by the bitwise and and the initial value for the builder will remain.
+                mFlags = (mFlags & ~capabilities.mCallerConfiguredFlags) |
+                        (capabilities.mFlags & capabilities.mCallerConfiguredFlags);
+                return this;
+            }
+
+            /**
+             * Returns a new {@code SignerConfig} instance configured based on the configuration of
+             * this builder.
+             */
+            public SignerCapabilities build() {
+                return new SignerCapabilities(mFlags, mCallerConfiguredFlags);
+            }
+        }
+    }
+
+    /**
+     * Configuration of a signer.  Used to add a new entry to the {@link SigningCertificateLineage}
+     *
+     * <p>Use {@link Builder} to obtain configuration instances.
+     */
+    public static class SignerConfig {
+        private final PrivateKey mPrivateKey;
+        private final X509Certificate mCertificate;
+
+        private SignerConfig(
+                PrivateKey privateKey,
+                X509Certificate certificate) {
+            mPrivateKey = privateKey;
+            mCertificate = certificate;
+        }
+
+        /**
+         * Returns the signing key of this signer.
+         */
+        public PrivateKey getPrivateKey() {
+            return mPrivateKey;
+        }
+
+        /**
+         * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+         * to this signer's private key.
+         */
+        public X509Certificate getCertificate() {
+            return mCertificate;
+        }
+
+        /**
+         * Builder of {@link SignerConfig} instances.
+         */
+        public static class Builder {
+            private final PrivateKey mPrivateKey;
+            private final X509Certificate mCertificate;
+
+            /**
+             * Constructs a new {@code Builder}.
+             *
+             * @param privateKey signing key
+             * @param certificate the X.509 certificate with a subject public key of the
+             * {@code privateKey}.
+             */
+            public Builder(
+                    PrivateKey privateKey,
+                    X509Certificate certificate) {
+                mPrivateKey = privateKey;
+                mCertificate = certificate;
+            }
+
+            /**
+             * Returns a new {@code SignerConfig} instance configured based on the configuration of
+             * this builder.
+             */
+            public SignerConfig build() {
+                return new SignerConfig(
+                        mPrivateKey,
+                        mCertificate);
+            }
+        }
+    }
+
+    /**
+     * Builder of {@link SigningCertificateLineage} instances.
+     */
+    public static class Builder {
+        private final SignerConfig mOriginalSignerConfig;
+        private final SignerConfig mNewSignerConfig;
+        private SignerCapabilities mOriginalCapabilities;
+        private SignerCapabilities mNewCapabilities;
+        private int mMinSdkVersion;
+        /**
+         * Constructs a new {@code Builder}.
+         *
+         * @param originalSignerConfig first signer in this lineage, parent of the next
+         * @param newSignerConfig new signer in the lineage; the new signing key that the APK will
+         *                        use
+         */
+        public Builder(
+                SignerConfig originalSignerConfig,
+                SignerConfig newSignerConfig) {
+            if (originalSignerConfig == null || newSignerConfig == null) {
+                throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
+                        + "new SigningCertificateLineage");
+            }
+            mOriginalSignerConfig = originalSignerConfig;
+            mNewSignerConfig = newSignerConfig;
+        }
+
+        /**
+         * Constructs a new {@code Builder} that is intended to create a {@code
+         * SigningCertificateLineage} with a single signer in the signing history.
+         *
+         * @param originalSignerConfig first signer in this lineage
+         */
+        public Builder(SignerConfig originalSignerConfig) {
+            if (originalSignerConfig == null) {
+                throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
+                        + "new SigningCertificateLineage");
+            }
+            mOriginalSignerConfig = originalSignerConfig;
+            mNewSignerConfig = null;
+        }
+
+        /**
+         * Sets the minimum Android platform version (API Level) on which this lineage is expected
+         * to validate.  It is possible that newer signers in the lineage may not be recognized on
+         * the given platform, but as long as an older signer is, the lineage can still be used to
+         * sign an APK for the given platform.
+         *
+         * <note> By default, this value is set to the value for the
+         * P release, since this structure was created for that release, and will also be set to
+         * that value if a smaller one is specified. </note>
+         */
+        public Builder setMinSdkVersion(int minSdkVersion) {
+            mMinSdkVersion = minSdkVersion;
+            return this;
+        }
+
+        /**
+         * Sets capabilities to give {@code mOriginalSignerConfig}. These capabilities allow an
+         * older signing certificate to still be used in some situations on the platform even though
+         * the APK is now being signed by a newer signing certificate.
+         */
+        public Builder setOriginalCapabilities(SignerCapabilities signerCapabilities) {
+            if (signerCapabilities == null) {
+                throw new NullPointerException("signerCapabilities == null");
+            }
+            mOriginalCapabilities = signerCapabilities;
+            return this;
+        }
+
+        /**
+         * Sets capabilities to give {@code mNewSignerConfig}. These capabilities allow an
+         * older signing certificate to still be used in some situations on the platform even though
+         * the APK is now being signed by a newer signing certificate.  By default, the new signer
+         * will have all capabilities, so when first switching to a new signing certificate, these
+         * capabilities have no effect, but they will act as the default level of trust when moving
+         * to a new signing certificate.
+         */
+        public Builder setNewCapabilities(SignerCapabilities signerCapabilities) {
+            if (signerCapabilities == null) {
+                throw new NullPointerException("signerCapabilities == null");
+            }
+            mNewCapabilities = signerCapabilities;
+            return this;
+        }
+
+        public SigningCertificateLineage build()
+                throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+                SignatureException {
+            if (mMinSdkVersion < AndroidSdkVersion.P) {
+                mMinSdkVersion = AndroidSdkVersion.P;
+            }
+
+            if (mOriginalCapabilities == null) {
+                mOriginalCapabilities = new SignerCapabilities.Builder().build();
+            }
+
+            if (mNewSignerConfig == null) {
+                return createSigningLineage(mMinSdkVersion, mOriginalSignerConfig,
+                        mOriginalCapabilities);
+            }
+
+            if (mNewCapabilities == null) {
+                mNewCapabilities = new SignerCapabilities.Builder().build();
+            }
+
+            return createSigningLineage(
+                    mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities,
+                    mNewSignerConfig, mNewCapabilities);
+        }
+    }
+}

+ 911 - 0
platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java

@@ -0,0 +1,911 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+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.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK source stamp verifier intended only to verify the validity of the stamp signature.
+ *
+ * <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
+ * when obtaining the digests for verification. This verifier should only be used in cases where
+ * another mechanism has already been used to verify the APK signatures.
+ */
+public class SourceStampVerifier {
+    private final File mApkFile;
+    private final DataSource mApkDataSource;
+
+    private final int mMinSdkVersion;
+    private final int mMaxSdkVersion;
+
+    private SourceStampVerifier(
+            File apkFile,
+            DataSource apkDataSource,
+            int minSdkVersion,
+            int maxSdkVersion) {
+        mApkFile = apkFile;
+        mApkDataSource = apkDataSource;
+        mMinSdkVersion = minSdkVersion;
+        mMaxSdkVersion = maxSdkVersion;
+    }
+
+    /**
+     * Verifies the APK's source stamp signature and returns the result of the verification.
+     *
+     * <p>The APK's source stamp can be considered verified if the result's {@link
+     * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
+     * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
+     * can be obtained as follows:
+     * <ul>
+     *     <li>Obtain the generic errors via {@link Result#getErrors()}
+     *     <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
+     *     query for any errors with {@link Result.SignerInfo#getErrors()}
+     *     <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
+     *     query for any errors with {@link Result.SignerInfo#getErrors()}
+     *     <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
+     *     for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
+     * </ul>
+     */
+    public SourceStampVerifier.Result verifySourceStamp() {
+        return verifySourceStamp(null);
+    }
+
+    /**
+     * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+     * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+     * of the verification.
+     *
+     * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+     * if present, without verifying the actual source stamp certificate used to sign the source
+     * stamp. This can be used to verify an APK contains a properly signed source stamp without
+     * verifying a particular signer.
+     *
+     * @see #verifySourceStamp()
+     */
+    public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
+        Closeable in = null;
+        try {
+            DataSource apk;
+            if (mApkDataSource != null) {
+                apk = mApkDataSource;
+            } else if (mApkFile != null) {
+                RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+                in = f;
+                apk = DataSources.asDataSource(f, 0, f.length());
+            } else {
+                throw new IllegalStateException("APK not provided");
+            }
+            return verifySourceStamp(apk, expectedCertDigest);
+        } catch (IOException e) {
+            Result result = new Result();
+            result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+            return result;
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+     * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+     * returns the result of the verification.
+     *
+     * @see #verifySourceStamp(String)
+     */
+    private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
+            String expectedCertDigest) {
+        Result result = new Result();
+        try {
+            ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+            // Attempt to obtain the source stamp's certificate digest from the APK.
+            List<CentralDirectoryRecord> cdRecords =
+                    ZipUtils.parseZipCentralDirectory(apk, zipSections);
+            CentralDirectoryRecord sourceStampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+                    sourceStampCdRecord = cdRecord;
+                    break;
+                }
+            }
+
+            // If the source stamp's certificate digest is not available within the APK then the
+            // source stamp cannot be verified; check if a source stamp signing block is in the
+            // APK's signature block to determine the appropriate status to return.
+            if (sourceStampCdRecord == null) {
+                boolean stampSigningBlockFound;
+                try {
+                    ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+                            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+                    stampSigningBlockFound = true;
+                } catch (SignatureNotFoundException e) {
+                    stampSigningBlockFound = false;
+                }
+                result.addVerificationError(stampSigningBlockFound
+                        ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
+                        : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+                return result;
+            }
+
+            // Verify that the contents of the source stamp certificate digest match the expected
+            // value, if provided.
+            byte[] sourceStampCertificateDigest =
+                    LocalFileRecord.getUncompressedData(
+                            apk,
+                            sourceStampCdRecord,
+                            zipSections.getZipCentralDirectoryOffset());
+            if (expectedCertDigest != null) {
+                String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
+                        sourceStampCertificateDigest);
+                if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+                    result.addVerificationError(
+                            ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+                            actualCertDigest, expectedCertDigest);
+                    return result;
+                }
+            }
+
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+                    new HashMap<>();
+            if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+                SignatureInfo signatureInfo;
+                try {
+                    signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+                } catch (SignatureNotFoundException e) {
+                    signatureInfo = null;
+                }
+                if (signatureInfo != null) {
+                    Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+                            ContentDigestAlgorithm.class);
+                    parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
+                            apkContentDigests, result);
+                    signatureSchemeApkContentDigests.put(
+                            VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
+                }
+            }
+
+            if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
+                    signatureSchemeApkContentDigests.isEmpty())) {
+                SignatureInfo signatureInfo;
+                try {
+                    signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+                            V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+                } catch (SignatureNotFoundException e) {
+                    signatureInfo = null;
+                }
+                if (signatureInfo != null) {
+                    Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+                            ContentDigestAlgorithm.class);
+                    parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
+                            apkContentDigests, result);
+                    signatureSchemeApkContentDigests.put(
+                            VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
+                }
+            }
+
+            if (mMinSdkVersion < AndroidSdkVersion.N
+                    || signatureSchemeApkContentDigests.isEmpty()) {
+                Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
+                        getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
+                signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+                        apkContentDigests);
+            }
+
+            ApkSigResult sourceStampResult =
+                    V2SourceStampVerifier.verify(
+                            apk,
+                            zipSections,
+                            sourceStampCertificateDigest,
+                            signatureSchemeApkContentDigests,
+                            mMinSdkVersion,
+                            mMaxSdkVersion);
+            result.mergeFrom(sourceStampResult);
+            return result;
+        } catch (ApkFormatException | IOException | ZipFormatException e) {
+            result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
+        } catch (NoSuchAlgorithmException e) {
+            result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+        } catch (SignatureNotFoundException e) {
+            result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+        }
+        return result;
+    }
+
+    /**
+     * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
+     * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the
+     * {@code [minSdkVersion, maxSdkVersion]} range.
+     */
+    public static void parseSigners(
+            ByteBuffer apkSignatureSchemeBlock,
+            int apkSigSchemeVersion,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            Result result) {
+        boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+        // Both the V2 and V3 signature blocks contain the following:
+        // * length-prefixed sequence of length-prefixed signers
+        ByteBuffer signers;
+        try {
+            signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
+        } catch (ApkFormatException e) {
+            result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+                    : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
+            return;
+        }
+        if (!signers.hasRemaining()) {
+            result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
+                    : ApkVerificationIssue.V3_SIG_NO_SIGNERS);
+            return;
+        }
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+        }
+        while (signers.hasRemaining()) {
+            Result.SignerInfo signerInfo = new Result.SignerInfo();
+            if (isV2Block) {
+                result.addV2Signer(signerInfo);
+            } else {
+                result.addV3Signer(signerInfo);
+            }
+            try {
+                ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
+                parseSigner(
+                        signer,
+                        apkSigSchemeVersion,
+                        certFactory,
+                        apkContentDigests,
+                        signerInfo);
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                signerInfo.addVerificationWarning(
+                        isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
+                                : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Parses the provided signer block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+     * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+     * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+     * integrity of the APK.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the
+     * {@code [minSdkVersion, maxSdkVersion]} range.
+     */
+    private static void parseSigner(
+            ByteBuffer signerBlock,
+            int apkSigSchemeVersion,
+            CertificateFactory certFactory,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            Result.SignerInfo signerInfo)
+            throws ApkFormatException {
+        boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+        // Both the V2 and V3 signer blocks contain the following:
+        // * length-prefixed signed data
+        //   * length-prefixed sequence of length-prefixed digests:
+        //     * uint32: signature algorithm ID
+        //     * length-prefixed bytes: digest of contents
+        //   * length-prefixed sequence of certificates:
+        //     * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+        ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
+        ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+        ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+
+        // Parse the digests block
+        while (digests.hasRemaining()) {
+            try {
+                ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
+                int sigAlgorithmId = digest.getInt();
+                byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                signerInfo.addVerificationWarning(
+                        isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
+                                : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
+                return;
+            }
+        }
+
+        // Parse the certificates block
+        if (certificates.hasRemaining()) {
+            byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
+            X509Certificate certificate;
+            try {
+                certificate = (X509Certificate) certFactory.generateCertificate(
+                        new ByteArrayInputStream(encodedCert));
+            } catch (CertificateException e) {
+                signerInfo.addVerificationWarning(
+                        isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
+                                : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
+                return;
+            }
+            // 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(certificate, encodedCert);
+            signerInfo.setSigningCertificate(certificate);
+        }
+
+        if (signerInfo.getSigningCertificate() == null) {
+            signerInfo.addVerificationWarning(
+                    isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
+                            : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
+            return;
+        }
+    }
+
+    /**
+     * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
+     * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
+     * returned.
+     *
+     * <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
+     * will be updated to include a warning, but the source stamp verification can still proceed.
+     */
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+            List<CentralDirectoryRecord> cdRecords,
+            DataSource apk,
+            ZipSections zipSections,
+            Result result)
+            throws IOException, ApkFormatException {
+        CentralDirectoryRecord manifestCdRecord = null;
+        List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
+        Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+                ContentDigestAlgorithm.class);
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            String cdRecordName = cdRecord.getName();
+            if (cdRecordName == null) {
+                continue;
+            }
+            if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
+                manifestCdRecord = cdRecord;
+                continue;
+            }
+            if (cdRecordName.startsWith("META-INF/")
+                    && (cdRecordName.endsWith(".RSA")
+                        || cdRecordName.endsWith(".DSA")
+                        || cdRecordName.endsWith(".EC"))) {
+                signatureBlockRecords.add(cdRecord);
+            }
+        }
+        if (manifestCdRecord == null) {
+            // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+            // digest is enough since this would affect the final digest signed by the stamp, and
+            // thus an empty digest will invalidate that signature.
+            return v1ContentDigest;
+        }
+        if (signatureBlockRecords.isEmpty()) {
+            result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+        } else {
+            for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
+                try {
+                    CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+                    byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
+                            signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
+                    for (Certificate certificate : certFactory.generateCertificates(
+                            new ByteArrayInputStream(signatureBlockBytes))) {
+                        // If multiple certificates are found within the signature block only the
+                        // first is used as the signer of this block.
+                        if (certificate instanceof X509Certificate) {
+                            Result.SignerInfo signerInfo = new Result.SignerInfo();
+                            signerInfo.setSigningCertificate((X509Certificate) certificate);
+                            result.addV1Signer(signerInfo);
+                            break;
+                        }
+                    }
+                } catch (CertificateException e) {
+                    // Log a warning for the parsing exception but still proceed with the stamp
+                    // verification.
+                    result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+                            signatureBlockRecord.getName(), e);
+                    break;
+                } catch (ZipFormatException e) {
+                    throw new ApkFormatException("Failed to read APK", e);
+                }
+            }
+        }
+        try {
+            byte[] manifestBytes =
+                    LocalFileRecord.getUncompressedData(
+                            apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+            v1ContentDigest.put(
+                    ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+            return v1ContentDigest;
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read APK", e);
+        }
+    }
+
+    /**
+     * Result of verifying the APK's source stamp signature; this signature can only be considered
+     * verified if {@link #isVerified()} returns true.
+     */
+    public static class Result {
+        private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
+        private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
+        private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
+        private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
+                mV2SchemeSigners, mV3SchemeSigners);
+        private SourceStampInfo mSourceStampInfo;
+
+        private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+        private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+        private boolean mVerified;
+
+        void addVerificationError(int errorId, Object... params) {
+            mErrors.add(new ApkVerificationIssue(errorId, params));
+        }
+
+        void addVerificationWarning(int warningId, Object... params) {
+            mWarnings.add(new ApkVerificationIssue(warningId, params));
+        }
+
+        private void addV1Signer(SignerInfo signerInfo) {
+            mV1SchemeSigners.add(signerInfo);
+        }
+
+        private void addV2Signer(SignerInfo signerInfo) {
+            mV2SchemeSigners.add(signerInfo);
+        }
+
+        private void addV3Signer(SignerInfo signerInfo) {
+            mV3SchemeSigners.add(signerInfo);
+        }
+
+        /**
+         * Returns {@code true} if the APK's source stamp signature
+         */
+        public boolean isVerified() {
+            return mVerified;
+        }
+
+        private void mergeFrom(ApkSigResult source) {
+            switch (source.signatureSchemeVersion) {
+                case Constants.VERSION_SOURCE_STAMP:
+                    mVerified = source.verified;
+                    if (!source.mSigners.isEmpty()) {
+                        mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown ApkSigResult Signing Block Scheme Id "
+                                    + source.signatureSchemeVersion);
+            }
+        }
+
+        /**
+         * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
+         * provided APK.
+         */
+        public List<SignerInfo> getV1SchemeSigners() {
+            return mV1SchemeSigners;
+        }
+
+        /**
+         * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
+         * provided APK.
+         */
+        public List<SignerInfo> getV2SchemeSigners() {
+            return mV2SchemeSigners;
+        }
+
+        /**
+         * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
+         * provided APK.
+         */
+        public List<SignerInfo> getV3SchemeSigners() {
+            return mV3SchemeSigners;
+        }
+
+        /**
+         * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
+         * APK, or null if the source stamp signature verification failed before the stamp signature
+         * block could be fully parsed.
+         */
+        public SourceStampInfo getSourceStampInfo() {
+            return mSourceStampInfo;
+        }
+
+        /**
+         * Returns {@code true} if an error was encountered while verifying the APK.
+         *
+         * <p>Any error prevents the APK from being considered verified.
+         */
+        public boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            for (List<SignerInfo> signers : mAllSchemeSigners) {
+                for (SignerInfo signer : signers) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                }
+            }
+            if (mSourceStampInfo != null) {
+                if (mSourceStampInfo.containsErrors()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Returns the errors encountered while verifying the APK's source stamp.
+         */
+        public List<ApkVerificationIssue> getErrors() {
+            return mErrors;
+        }
+
+        /**
+         * Returns the warnings encountered while verifying the APK's source stamp.
+         */
+        public List<ApkVerificationIssue> getWarnings() {
+            return mWarnings;
+        }
+
+        /**
+         * Returns all errors for this result, including any errors from signature scheme signers
+         * and the source stamp.
+         */
+        public List<ApkVerificationIssue> getAllErrors() {
+            List<ApkVerificationIssue> errors = new ArrayList<>();
+            errors.addAll(mErrors);
+
+            for (List<SignerInfo> signers : mAllSchemeSigners) {
+                for (SignerInfo signer : signers) {
+                    errors.addAll(signer.getErrors());
+                }
+            }
+            if (mSourceStampInfo != null) {
+                errors.addAll(mSourceStampInfo.getErrors());
+            }
+            return errors;
+        }
+
+        /**
+         * Returns all warnings for this result, including any warnings from signature scheme
+         * signers and the source stamp.
+         */
+        public List<ApkVerificationIssue> getAllWarnings() {
+            List<ApkVerificationIssue> warnings = new ArrayList<>();
+            warnings.addAll(mWarnings);
+
+            for (List<SignerInfo> signers : mAllSchemeSigners) {
+                for (SignerInfo signer : signers) {
+                    warnings.addAll(signer.getWarnings());
+                }
+            }
+            if (mSourceStampInfo != null) {
+                warnings.addAll(mSourceStampInfo.getWarnings());
+            }
+            return warnings;
+        }
+
+        /**
+         * Contains information about an APK's signer and any errors encountered while parsing the
+         * corresponding signature block.
+         */
+        public static class SignerInfo {
+            private X509Certificate mSigningCertificate;
+            private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+            private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+            void setSigningCertificate(X509Certificate signingCertificate) {
+                mSigningCertificate = signingCertificate;
+            }
+
+            void addVerificationError(int errorId, Object... params) {
+                mErrors.add(new ApkVerificationIssue(errorId, params));
+            }
+
+            void addVerificationWarning(int warningId, Object... params) {
+                mWarnings.add(new ApkVerificationIssue(warningId, params));
+            }
+
+            /**
+             * Returns the current signing certificate used by this signer.
+             */
+            public X509Certificate getSigningCertificate() {
+                return mSigningCertificate;
+            }
+
+            /**
+             * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
+             * encountered during processing of this signer's signature block.
+             */
+            public List<ApkVerificationIssue> getErrors() {
+                return mErrors;
+            }
+
+            /**
+             * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
+             * encountered during processing of this signer's signature block.
+             */
+            public List<ApkVerificationIssue> getWarnings() {
+                return mWarnings;
+            }
+
+            /**
+             * Returns {@code true} if any errors were encountered while parsing this signer's
+             * signature block.
+             */
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+        }
+
+        /**
+         * Contains information about an APK's source stamp and any errors encountered while
+         * parsing the stamp signature block.
+         */
+        public static class SourceStampInfo {
+            private final List<X509Certificate> mCertificates;
+            private final List<X509Certificate> mCertificateLineage;
+
+            private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+            private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+            private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
+
+            private final long mTimestamp;
+
+            /*
+             * Since this utility is intended just to verify the source stamp, and the source stamp
+             * currently only logs warnings to prevent failing the APK signature verification, treat
+             * all warnings as errors. If the stamp verification is updated to log errors this
+             * should be set to false to ensure only errors trigger a failure verifying the source
+             * stamp.
+             */
+            private static final boolean mWarningsAsErrors = true;
+
+            private SourceStampInfo(ApkSignerInfo result) {
+                mCertificates = result.certs;
+                mCertificateLineage = result.certificateLineage;
+                mErrors.addAll(result.getErrors());
+                mWarnings.addAll(result.getWarnings());
+                mInfoMessages.addAll(result.getInfoMessages());
+                mTimestamp = result.timestamp;
+            }
+
+            /**
+             * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the SourceStamp's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCertificates.isEmpty() ? null : mCertificates.get(0);
+            }
+
+            /**
+             * Returns a {@code List} of {@link X509Certificate} instances representing the source
+             * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
+             * if the stamp's signing certificate has not been rotated.
+             */
+            public List<X509Certificate> getCertificatesInLineage() {
+                return mCertificateLineage;
+            }
+
+            /**
+             * Returns whether any errors were encountered during the source stamp verification.
+             */
+            public boolean containsErrors() {
+                return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
+            }
+
+            /**
+             * Returns {@code true} if any info messages were encountered during verification of
+             * this source stamp.
+             */
+            public boolean containsInfoMessages() {
+                return !mInfoMessages.isEmpty();
+            }
+
+            /**
+             * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
+             * encountered during source stamp verification.
+             */
+            public List<ApkVerificationIssue> getErrors() {
+                if (!mWarningsAsErrors) {
+                    return mErrors;
+                }
+                List<ApkVerificationIssue> result = new ArrayList<>();
+                result.addAll(mErrors);
+                result.addAll(mWarnings);
+                return result;
+            }
+
+            /**
+             * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
+             * were encountered during source stamp verification.
+             */
+            public List<ApkVerificationIssue> getWarnings() {
+                return mWarnings;
+            }
+
+            /**
+             * Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
+             * that were encountered during source stamp verification.
+             */
+            public List<ApkVerificationIssue> getInfoMessages() {
+                return mInfoMessages;
+            }
+
+            /**
+             * Returns the epoch timestamp in seconds representing the time this source stamp block
+             * was signed, or 0 if the timestamp is not available.
+             */
+            public long getTimestampEpochSeconds() {
+                return mTimestamp;
+            }
+        }
+    }
+
+    /**
+     * Builder of {@link SourceStampVerifier} instances.
+     *
+     * <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
+     * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
+     * queried to determine the APK's minimum supported level, so the caller should specify a lower
+     * bound with {@link #setMinCheckedPlatformVersion(int)}.
+     */
+    public static class Builder {
+        private final File mApkFile;
+        private final DataSource mApkDataSource;
+
+        private int mMinSdkVersion = 1;
+        private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+        /**
+         * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+         * apk}.
+         */
+        public Builder(File apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkFile = apk;
+            mApkDataSource = null;
+        }
+
+        /**
+         * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+         * apk}.
+         */
+        public Builder(DataSource apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkDataSource = apk;
+            mApkFile = null;
+        }
+
+        /**
+         * Sets the oldest Android platform version for which the APK's source stamp is verified.
+         *
+         * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+         * on all Android platforms starting from the platform version with the provided {@code
+         * minSdkVersion}. The upper end of the platform versions range can be modified via
+         * {@link #setMaxCheckedPlatformVersion(int)}.
+         *
+         * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+         */
+        public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+            mMinSdkVersion = minSdkVersion;
+            return this;
+        }
+
+        /**
+         * Sets the newest Android platform version for which the APK's source stamp  is verified.
+         *
+         * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+         * on all platform versions up to and including the proviced {@code maxSdkVersion}. The
+         * lower end of the platform versions range can be modified via {@link
+         * #setMinCheckedPlatformVersion(int)}.
+         *
+         * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+         * @see #setMinCheckedPlatformVersion(int)
+         */
+        public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+            mMaxSdkVersion = maxSdkVersion;
+            return this;
+        }
+
+        /**
+         * Returns a {@link SourceStampVerifier} initialized according to the configuration of this
+         * builder.
+         */
+        public SourceStampVerifier build() {
+            return new SourceStampVerifier(
+                    mApkFile,
+                    mApkDataSource,
+                    mMinSdkVersion,
+                    mMaxSdkVersion);
+        }
+    }
+}

+ 35 - 0
platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+/**
+ * Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
+ * well-formed ZIP archive, in which case {@link #getCause()} will return a
+ * {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
+ * multiple ZIP entries with the same name.
+ */
+public class ApkFormatException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public ApkFormatException(String message) {
+        super(message);
+    }
+
+    public ApkFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 32 - 0
platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+/**
+ * Indicates that no APK Signing Block was found in an APK.
+ */
+public class ApkSigningBlockNotFoundException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public ApkSigningBlockNotFoundException(String message) {
+        super(message);
+    }
+
+    public ApkSigningBlockNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 670 - 0
platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java

@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+import com.android.apksig.internal.apk.AndroidBinXmlParser;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * APK utilities.
+ */
+public abstract class ApkUtils {
+
+    /**
+     * Name of the Android manifest ZIP entry in APKs.
+     */
+    public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
+
+    /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
+    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
+            SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
+    private ApkUtils() {}
+
+    /**
+     * Finds the main ZIP sections of the provided APK.
+     *
+     * @throws IOException if an I/O error occurred while reading the APK
+     * @throws ZipFormatException if the APK is malformed
+     */
+    public static ZipSections findZipSections(DataSource apk)
+            throws IOException, ZipFormatException {
+        com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+        return new ZipSections(
+                zipSections.getZipCentralDirectoryOffset(),
+                zipSections.getZipCentralDirectorySizeBytes(),
+                zipSections.getZipCentralDirectoryRecordCount(),
+                zipSections.getZipEndOfCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectory());
+    }
+
+    /**
+     * Information about the ZIP sections of an APK.
+     */
+    public static class ZipSections extends com.android.apksig.zip.ZipSections {
+        public ZipSections(
+                long centralDirectoryOffset,
+                long centralDirectorySizeBytes,
+                int centralDirectoryRecordCount,
+                long eocdOffset,
+                ByteBuffer eocd) {
+            super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
+                    eocdOffset, eocd);
+        }
+    }
+
+    /**
+     * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
+     * Directory record.
+     *
+     * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
+     * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
+     *        be between {@code 0} and {@code 2^32 - 1} inclusive.
+     */
+    public static void setZipEocdCentralDirectoryOffset(
+            ByteBuffer zipEndOfCentralDirectory, long offset) {
+        ByteBuffer eocd = zipEndOfCentralDirectory.slice();
+        eocd.order(ByteOrder.LITTLE_ENDIAN);
+        ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
+    }
+
+    /**
+     * Updates the length of EOCD comment.
+     *
+     * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
+     */
+    public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
+        ByteBuffer eocd = zipEndOfCentralDirectory.slice();
+        eocd.order(ByteOrder.LITTLE_ENDIAN);
+        ZipUtils.updateZipEocdCommentLen(eocd);
+    }
+
+    /**
+     * Returns the APK Signing Block of the provided {@code apk}.
+     *
+     * @throws ApkFormatException if the APK is not a valid ZIP archive
+     * @throws IOException if an I/O error occurs
+     * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+     *
+     * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+     * </a>
+     */
+    public static ApkSigningBlock findApkSigningBlock(DataSource apk)
+            throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
+        ApkUtils.ZipSections inputZipSections;
+        try {
+            inputZipSections = ApkUtils.findZipSections(apk);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+        }
+        return findApkSigningBlock(apk, inputZipSections);
+    }
+
+    /**
+     * Returns the APK Signing Block of the provided APK.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+     *
+     * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+     * </a>
+     */
+    public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
+            throws IOException, ApkSigningBlockNotFoundException {
+        ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
+                zipSections);
+        return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
+    }
+
+    /**
+     * Information about the location of the APK Signing Block inside an APK.
+     */
+    public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
+        /**
+         * Constructs a new {@code ApkSigningBlock}.
+         *
+         * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
+         *        Signing Block inside the APK file
+         * @param contents contents of the APK Signing Block
+         */
+        public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
+            super(startOffsetInApk, contents);
+        }
+    }
+
+    /**
+     * Returns the contents of the APK's {@code AndroidManifest.xml}.
+     *
+     * @throws IOException if an I/O error occurs while reading the APK
+     * @throws ApkFormatException if the APK is malformed
+     */
+    public static ByteBuffer getAndroidManifest(DataSource apk)
+            throws IOException, ApkFormatException {
+        ZipSections zipSections;
+        try {
+            zipSections = findZipSections(apk);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Not a valid ZIP archive", e);
+        }
+        List<CentralDirectoryRecord> cdRecords =
+                V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+        CentralDirectoryRecord androidManifestCdRecord = null;
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+                androidManifestCdRecord = cdRecord;
+                break;
+            }
+        }
+        if (androidManifestCdRecord == null) {
+            throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
+        }
+        DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
+
+        try {
+            return ByteBuffer.wrap(
+                    LocalFileRecord.getUncompressedData(
+                            lfhSection, androidManifestCdRecord, lfhSection.size()));
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
+        }
+    }
+
+    /**
+     * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
+     */
+    private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
+
+    /**
+     * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
+     */
+    private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
+
+    /**
+     * Android resource ID of the {@code android:targetSandboxVersion} attribute in
+     * AndroidManifest.xml.
+     */
+    private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
+
+    /**
+     * Android resource ID of the {@code android:targetSdkVersion} attribute in
+     * AndroidManifest.xml.
+     */
+    private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
+    private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
+
+    /**
+     * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
+     */
+    private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
+    private static final String MANIFEST_ELEMENT_TAG = "manifest";
+
+    /**
+     * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
+     */
+    private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
+
+    /**
+     * Returns the lowest Android platform version (API Level) supported by an APK with the
+     * provided {@code AndroidManifest.xml}.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *        resource format
+     *
+     * @throws MinSdkVersionException if an error occurred while determining the API Level
+     */
+    public static int getMinSdkVersionFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) throws MinSdkVersionException {
+        // IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
+        // uses-sdk elements which are children of the top-level manifest element. uses-sdk element
+        // declares the minimum supported platform version using the android:minSdkVersion attribute
+        // whose default value is 1.
+        // For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
+        // is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
+        // effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
+
+        try {
+            // If no uses-sdk elements are encountered, Android accepts the APK. We treat this
+            // scenario as though the minimum supported API Level is 1.
+            int result = 1;
+
+            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+            int eventType = parser.getEventType();
+            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+                        && (parser.getDepth() == 2)
+                        && ("uses-sdk".equals(parser.getName()))
+                        && (parser.getNamespace().isEmpty())) {
+                    // In each uses-sdk element, minSdkVersion defaults to 1
+                    int minSdkVersion = 1;
+                    for (int i = 0; i < parser.getAttributeCount(); i++) {
+                        if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
+                            int valueType = parser.getAttributeValueType(i);
+                            switch (valueType) {
+                                case AndroidBinXmlParser.VALUE_TYPE_INT:
+                                    minSdkVersion = parser.getAttributeIntValue(i);
+                                    break;
+                                case AndroidBinXmlParser.VALUE_TYPE_STRING:
+                                    minSdkVersion =
+                                            getMinSdkVersionForCodename(
+                                                    parser.getAttributeStringValue(i));
+                                    break;
+                                default:
+                                    throw new MinSdkVersionException(
+                                            "Unable to determine APK's minimum supported Android"
+                                                    + ": unsupported value type in "
+                                                    + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+                                                    + " minSdkVersion"
+                                                    + ". Only integer values supported.");
+                            }
+                            break;
+                        }
+                    }
+                    result = Math.max(result, minSdkVersion);
+                }
+                eventType = parser.next();
+            }
+
+            return result;
+        } catch (AndroidBinXmlParser.XmlParserException e) {
+            throw new MinSdkVersionException(
+                    "Unable to determine APK's minimum supported Android platform version"
+                            + ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
+                    e);
+        }
+    }
+
+    private static class CodenamesLazyInitializer {
+
+        /**
+         * List of platform codename (first letter of) to API Level mappings. The list must be
+         * sorted by the first letter. For codenames not in the list, the assumption is that the API
+         * Level is incremented by one for every increase in the codename's first letter.
+         */
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
+                new Pair[] {
+            Pair.of('C', 2),
+            Pair.of('D', 3),
+            Pair.of('E', 4),
+            Pair.of('F', 7),
+            Pair.of('G', 8),
+            Pair.of('H', 10),
+            Pair.of('I', 13),
+            Pair.of('J', 15),
+            Pair.of('K', 18),
+            Pair.of('L', 20),
+            Pair.of('M', 22),
+            Pair.of('N', 23),
+            Pair.of('O', 25),
+        };
+
+        private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
+                new ByFirstComparator();
+
+        private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
+            @Override
+            public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
+                char c1 = o1.getFirst();
+                char c2 = o2.getFirst();
+                return c1 - c2;
+            }
+        }
+    }
+
+    /**
+     * Returns the API Level corresponding to the provided platform codename.
+     *
+     * <p>This method is pessimistic. It returns a value one lower than the API Level with which the
+     * platform is actually released (e.g., 23 for N which was released as API Level 24). This is
+     * because new features which first appear in an API Level are not available in the early days
+     * of that platform version's existence, when the platform only has a codename. Moreover, this
+     * method currently doesn't differentiate between initial and MR releases, meaning API Level
+     * returned for MR releases may be more than one lower than the API Level with which the
+     * platform version is actually released.
+     *
+     * @throws CodenameMinSdkVersionException if the {@code codename} is not supported
+     */
+    static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
+        char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
+        // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
+        // We only look at the first letter of the codename as this is the most important letter.
+        if ((firstChar >= 'A') && (firstChar <= 'Z')) {
+            Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
+                    CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
+            int searchResult =
+                    Arrays.binarySearch(
+                            sortedCodenamesFirstCharToApiLevel,
+                            Pair.of(firstChar, null), // second element of the pair is ignored here
+                            CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
+            if (searchResult >= 0) {
+                // Exact match -- searchResult is the index of the matching element
+                return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
+            }
+            // Not an exact match -- searchResult is negative and is -(insertion index) - 1.
+            // The element at insertionIndex - 1 (if present) is smaller than firstChar and the
+            // element at insertionIndex (if present) is greater than firstChar.
+            int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
+            if (insertionIndex == 0) {
+                // 'A' or 'B' -- never released to public
+                return 1;
+            } else {
+                // The element at insertionIndex - 1 is the newest older codename.
+                // API Level bumped by at least 1 for every change in the first letter of codename
+                Pair<Character, Integer> newestOlderCodenameMapping =
+                        sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
+                char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
+                int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
+                return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
+            }
+        }
+
+        throw new CodenameMinSdkVersionException(
+                "Unable to determine APK's minimum supported Android platform version"
+                        + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
+                        + "'s minSdkVersion: \"" + codename + "\"",
+                codename);
+    }
+
+    /**
+     * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
+     * See the {@code android:debuggable} attribute of the {@code application} element.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *        resource format
+     *
+     * @throws ApkFormatException if the manifest is malformed
+     */
+    public static boolean getDebuggableFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) throws ApkFormatException {
+        // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
+        // "application" element which is a child of the top-level manifest element. The debuggable
+        // attribute of this application element is coerced to a boolean value. If there is no
+        // application element or if it doesn't declare the debuggable attribute, the package is
+        // considered not debuggable.
+
+        try {
+            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+            int eventType = parser.getEventType();
+            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+                        && (parser.getDepth() == 2)
+                        && ("application".equals(parser.getName()))
+                        && (parser.getNamespace().isEmpty())) {
+                    for (int i = 0; i < parser.getAttributeCount(); i++) {
+                        if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
+                            int valueType = parser.getAttributeValueType(i);
+                            switch (valueType) {
+                                case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
+                                case AndroidBinXmlParser.VALUE_TYPE_STRING:
+                                case AndroidBinXmlParser.VALUE_TYPE_INT:
+                                    String value = parser.getAttributeStringValue(i);
+                                    return ("true".equals(value))
+                                            || ("TRUE".equals(value))
+                                            || ("1".equals(value));
+                                case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
+                                    // References to resources are not supported on purpose. The
+                                    // reason is that the resolved value depends on the resource
+                                    // configuration (e.g, MNC/MCC, locale, screen density) used
+                                    // at resolution time. As a result, the same APK may appear as
+                                    // debuggable in one situation and as non-debuggable in another
+                                    // situation. Such APKs may put users at risk.
+                                    throw new ApkFormatException(
+                                            "Unable to determine whether APK is debuggable"
+                                                    + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+                                                    + " android:debuggable attribute references a"
+                                                    + " resource. References are not supported for"
+                                                    + " security reasons. Only constant boolean,"
+                                                    + " string and int values are supported.");
+                                default:
+                                    throw new ApkFormatException(
+                                            "Unable to determine whether APK is debuggable"
+                                                    + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+                                                    + " android:debuggable attribute uses"
+                                                    + " unsupported value type. Only boolean,"
+                                                    + " string and int values are supported.");
+                            }
+                        }
+                    }
+                    // This application element does not declare the debuggable attribute
+                    return false;
+                }
+                eventType = parser.next();
+            }
+
+            // No application element found
+            return false;
+        } catch (AndroidBinXmlParser.XmlParserException e) {
+            throw new ApkFormatException(
+                    "Unable to determine whether APK is debuggable: malformed binary resource: "
+                            + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
+                    e);
+        }
+    }
+
+    /**
+     * Returns the package name of the APK according to its {@code AndroidManifest.xml} or
+     * {@code null} if package name is not declared. See the {@code package} attribute of the
+     * {@code manifest} element.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *        resource format
+     *
+     * @throws ApkFormatException if the manifest is malformed
+     */
+    public static String getPackageNameFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) throws ApkFormatException {
+        // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
+        // manifest element. Interestingly, as opposed to most other attributes, Android Package
+        // Manager looks up this attribute by its name rather than by its resource ID.
+
+        try {
+            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+            int eventType = parser.getEventType();
+            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+                        && (parser.getDepth() == 1)
+                        && ("manifest".equals(parser.getName()))
+                        && (parser.getNamespace().isEmpty())) {
+                    for (int i = 0; i < parser.getAttributeCount(); i++) {
+                        if ("package".equals(parser.getAttributeName(i))
+                                && (parser.getNamespace().isEmpty())) {
+                            return parser.getAttributeStringValue(i);
+                        }
+                    }
+                    // No "package" attribute found
+                    return null;
+                }
+                eventType = parser.next();
+            }
+
+            // No manifest element found
+            return null;
+        } catch (AndroidBinXmlParser.XmlParserException e) {
+            throw new ApkFormatException(
+                    "Unable to determine APK package name: malformed binary resource: "
+                            + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
+                    e);
+        }
+    }
+
+    /**
+     * Returns the security sandbox version targeted by an APK with the provided
+     * {@code AndroidManifest.xml}.
+     *
+     * <p>If the security sandbox version is not specified in the manifest a default value of 1 is
+     * returned.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     */
+    public static int getTargetSandboxVersionFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) {
+        try {
+            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                    MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
+        } catch (ApkFormatException e) {
+            // An ApkFormatException indicates the target sandbox is not specified in the manifest;
+            // return a default value of 1.
+            return 1;
+        }
+    }
+
+    /**
+     * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
+     *
+     * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
+     * value is specified then a value of 1 is returned.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     */
+    public static int getTargetSdkVersionFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) {
+        // If the targetSdkVersion is not specified then the platform will use the value of the
+        // minSdkVersion; if neither is specified then the platform will use a value of 1.
+        int minSdkVersion = 1;
+        try {
+            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                    USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
+        } catch (ApkFormatException e) {
+            // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
+            // element is not specified at all.
+        }
+        androidManifestContents.rewind();
+        try {
+            minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
+        } catch (ApkFormatException e) {
+            // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
+            // the uses-sdk element is not specified at all.
+        }
+        return minSdkVersion;
+    }
+
+    /**
+     * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
+     *
+     * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
+     * integer an ApkFormatException is thrown.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     * @throws ApkFormatException if an error occurred while determining the versionCode, or if the
+     *                            versionCode attribute value is not available.
+     */
+    public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
+            throws ApkFormatException {
+        return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
+    }
+
+    /**
+     * Returns the versionCode and versionCodeMajor of the APK according to its {@code
+     * AndroidManifest.xml} combined together as a single long value.
+     *
+     * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
+     * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     * @throws ApkFormatException if an error occurred while determining the version, or if the
+     *                            versionCode attribute value is not available.
+     */
+    public static long getLongVersionCodeFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) throws ApkFormatException {
+        // If the versionCode is not found then allow the ApkFormatException to be thrown to notify
+        // the caller that the versionCode is not available.
+        int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
+        long versionCodeMajor = 0;
+        try {
+            androidManifestContents.rewind();
+            versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                    MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
+        } catch (ApkFormatException e) {
+            // This is expected if the versionCodeMajor has not been defined for the APK; in this
+            // case the return value is just the versionCode.
+        }
+        return (versionCodeMajor << 32) | versionCode;
+    }
+
+    /**
+     * Returns the integer value of the requested {@code attributeId} in the specified {@code
+     * elementName} from the provided {@code androidManifestContents} in binary Android resource
+     * format.
+     *
+     * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
+     *                            if the requested attribute is not found.
+     */
+    private static int getAttributeValueFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents, String elementName, int attributeId)
+            throws ApkFormatException {
+        if (elementName == null) {
+            throw new NullPointerException("elementName cannot be null");
+        }
+        try {
+            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+            int eventType = parser.getEventType();
+            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+                        && (elementName.equals(parser.getName()))) {
+                    for (int i = 0; i < parser.getAttributeCount(); i++) {
+                        if (parser.getAttributeNameResourceId(i) == attributeId) {
+                            int valueType = parser.getAttributeValueType(i);
+                            switch (valueType) {
+                                case AndroidBinXmlParser.VALUE_TYPE_INT:
+                                case AndroidBinXmlParser.VALUE_TYPE_STRING:
+                                    return parser.getAttributeIntValue(i);
+                                default:
+                                    throw new ApkFormatException(
+                                            "Unsupported value type, " + valueType
+                                                    + ", for attribute " + String.format("0x%08X",
+                                                    attributeId) + " under element " + elementName);
+
+                            }
+                        }
+                    }
+                }
+                eventType = parser.next();
+            }
+            throw new ApkFormatException(
+                    "Failed to determine APK's " + elementName + " attribute "
+                            + String.format("0x%08X", attributeId) + " value");
+        } catch (AndroidBinXmlParser.XmlParserException e) {
+            throw new ApkFormatException(
+                    "Unable to determine value for attribute " + String.format("0x%08X",
+                            attributeId) + " under element " + elementName
+                            + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
+        }
+    }
+
+    public static byte[] computeSha256DigestBytes(byte[] data) {
+        return ApkUtilsLite.computeSha256DigestBytes(data);
+    }
+}

+ 199 - 0
platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java

@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Lightweight version of the ApkUtils for clients that only require a subset of the utility
+ * functionality.
+ */
+public class ApkUtilsLite {
+    private ApkUtilsLite() {}
+
+    /**
+     * Finds the main ZIP sections of the provided APK.
+     *
+     * @throws IOException if an I/O error occurred while reading the APK
+     * @throws ZipFormatException if the APK is malformed
+     */
+    public static ZipSections findZipSections(DataSource apk)
+            throws IOException, ZipFormatException {
+        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+        if (eocdAndOffsetInFile == null) {
+            throw new ZipFormatException("ZIP End of Central Directory record not found");
+        }
+
+        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
+        long eocdOffset = eocdAndOffsetInFile.getSecond();
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
+        if (cdStartOffset > eocdOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory start offset out of range: " + cdStartOffset
+                            + ". ZIP End of Central Directory offset: " + eocdOffset);
+        }
+
+        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
+        long cdEndOffset = cdStartOffset + cdSizeBytes;
+        if (cdEndOffset > eocdOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory overlaps with End of Central Directory"
+                            + ". CD end: " + cdEndOffset
+                            + ", EoCD start: " + eocdOffset);
+        }
+
+        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
+
+        return new ZipSections(
+                cdStartOffset,
+                cdSizeBytes,
+                cdRecordCount,
+                eocdOffset,
+                eocdBuf);
+    }
+
+    // See https://source.android.com/security/apksigning/v2.html
+    private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+    private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+    private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+    /**
+     * Returns the APK Signing Block of the provided APK.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+     *
+     * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+     * </a>
+     */
+    public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
+            throws IOException, ApkSigningBlockNotFoundException {
+        // FORMAT (see https://source.android.com/security/apksigning/v2.html):
+        // OFFSET       DATA TYPE  DESCRIPTION
+        // * @+0  bytes uint64:    size in bytes (excluding this field)
+        // * @+8  bytes payload
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+
+        long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+        long centralDirEndOffset =
+                centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+        long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+        if (centralDirEndOffset != eocdStartOffset) {
+            throw new ApkSigningBlockNotFoundException(
+                    "ZIP Central Directory is not immediately followed by End of Central Directory"
+                            + ". CD end: " + centralDirEndOffset
+                            + ", EoCD start: " + eocdStartOffset);
+        }
+
+        if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK too small for APK Signing Block. ZIP Central Directory offset: "
+                            + centralDirStartOffset);
+        }
+        // Read the magic and offset in file from the footer section of the block:
+        // * uint64:   size of block
+        // * 16 bytes: magic
+        ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
+        footer.order(ByteOrder.LITTLE_ENDIAN);
+        if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+                || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+            throw new ApkSigningBlockNotFoundException(
+                    "No APK Signing Block before ZIP Central Directory");
+        }
+        // Read and compare size fields
+        long apkSigBlockSizeInFooter = footer.getLong(0);
+        if ((apkSigBlockSizeInFooter < footer.capacity())
+                || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+        }
+        int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+        long apkSigBlockOffset = centralDirStartOffset - totalSize;
+        if (apkSigBlockOffset < 0) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK Signing Block offset out of range: " + apkSigBlockOffset);
+        }
+        ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
+        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+        long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+        if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK Signing Block sizes in header and footer do not match: "
+                            + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+        }
+        return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+    }
+
+    /**
+     * Information about the location of the APK Signing Block inside an APK.
+     */
+    public static class ApkSigningBlock {
+        private final long mStartOffsetInApk;
+        private final DataSource mContents;
+
+        /**
+         * Constructs a new {@code ApkSigningBlock}.
+         *
+         * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
+         *        Signing Block inside the APK file
+         * @param contents contents of the APK Signing Block
+         */
+        public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
+            mStartOffsetInApk = startOffsetInApk;
+            mContents = contents;
+        }
+
+        /**
+         * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
+         */
+        public long getStartOffset() {
+            return mStartOffsetInApk;
+        }
+
+        /**
+         * Returns the data source which provides the full contents of the APK Signing Block,
+         * including its footer.
+         */
+        public DataSource getContents() {
+            return mContents;
+        }
+    }
+
+    public static byte[] computeSha256DigestBytes(byte[] data) {
+        MessageDigest messageDigest;
+        try {
+            messageDigest = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException("SHA-256 is not found", e);
+        }
+        messageDigest.update(data);
+        return messageDigest.digest();
+    }
+}

+ 46 - 0
platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+/**
+ * Indicates that there was an issue determining the minimum Android platform version supported by
+ * an APK because the version is specified as a codename, rather than as API Level number, and the
+ * codename is in an unexpected format.
+ */
+public class CodenameMinSdkVersionException extends MinSdkVersionException {
+
+    private static final long serialVersionUID = 1L;
+
+    /** Encountered codename. */
+    private final String mCodename;
+
+    /**
+     * Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
+     * codename.
+     */
+    public CodenameMinSdkVersionException(String message, String codename) {
+        super(message);
+        mCodename = codename;
+    }
+
+    /**
+     * Returns the codename.
+     */
+    public String getCodename() {
+        return mCodename;
+    }
+}

+ 40 - 0
platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+/**
+ * Indicates that there was an issue determining the minimum Android platform version supported by
+ * an APK.
+ */
+public class MinSdkVersionException extends ApkFormatException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new {@code MinSdkVersionException} with the provided message.
+     */
+    public MinSdkVersionException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new {@code MinSdkVersionException} with the provided message and cause.
+     */
+    public MinSdkVersionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 869 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java

@@ -0,0 +1,869 @@
+/*
+ * Copyright (C) 2016 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.apk;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
+ *
+ * <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
+ * {@link #getEventType()} and {@link #next()} methods. Additional information about the current
+ * event can be obtained via an assortment of getters, for example, {@link #getName()} or
+ * {@link #getAttributeNameResourceId(int)}.
+ */
+public class AndroidBinXmlParser {
+
+    /** Event: start of document. */
+    public static final int EVENT_START_DOCUMENT = 1;
+
+    /** Event: end of document. */
+    public static final int EVENT_END_DOCUMENT = 2;
+
+    /** Event: start of an element. */
+    public static final int EVENT_START_ELEMENT = 3;
+
+    /** Event: end of an document. */
+    public static final int EVENT_END_ELEMENT = 4;
+
+    /** Attribute value type is not supported by this parser. */
+    public static final int VALUE_TYPE_UNSUPPORTED = 0;
+
+    /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
+    public static final int VALUE_TYPE_STRING = 1;
+
+    /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
+    public static final int VALUE_TYPE_INT = 2;
+
+    /**
+     * Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
+     */
+    public static final int VALUE_TYPE_REFERENCE = 3;
+
+    /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
+    public static final int VALUE_TYPE_BOOLEAN = 4;
+
+    private static final long NO_NAMESPACE = 0xffffffffL;
+
+    private final ByteBuffer mXml;
+
+    private StringPool mStringPool;
+    private ResourceMap mResourceMap;
+    private int mDepth;
+    private int mCurrentEvent = EVENT_START_DOCUMENT;
+
+    private String mCurrentElementName;
+    private String mCurrentElementNamespace;
+    private int mCurrentElementAttributeCount;
+    private List<Attribute> mCurrentElementAttributes;
+    private ByteBuffer mCurrentElementAttributesContents;
+    private int mCurrentElementAttrSizeBytes;
+
+    /**
+     * Constructs a new parser for the provided document.
+     */
+    public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
+        xml.order(ByteOrder.LITTLE_ENDIAN);
+
+        Chunk resXmlChunk = null;
+        while (xml.hasRemaining()) {
+            Chunk chunk = Chunk.get(xml);
+            if (chunk == null) {
+                break;
+            }
+            if (chunk.getType() == Chunk.TYPE_RES_XML) {
+                resXmlChunk = chunk;
+                break;
+            }
+        }
+
+        if (resXmlChunk == null) {
+            throw new XmlParserException("No XML chunk in file");
+        }
+        mXml = resXmlChunk.getContents();
+    }
+
+    /**
+     * Returns the depth of the current element. Outside of the root of the document the depth is
+     * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
+     * is decremented by {@code 1} after each {@code end element} event.
+     */
+    public int getDepth() {
+        return mDepth;
+    }
+
+    /**
+     * Returns the type of the current event. See {@code EVENT_...} constants.
+     */
+    public int getEventType() {
+        return mCurrentEvent;
+    }
+
+    /**
+     * Returns the local name of the current element or {@code null} if the current event does not
+     * pertain to an element.
+     */
+    public String getName() {
+        if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
+            return null;
+        }
+        return mCurrentElementName;
+    }
+
+    /**
+     * Returns the namespace of the current element or {@code null} if the current event does not
+     * pertain to an element. Returns an empty string if the element is not associated with a
+     * namespace.
+     */
+    public String getNamespace() {
+        if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
+            return null;
+        }
+        return mCurrentElementNamespace;
+    }
+
+    /**
+     * Returns the number of attributes of the element associated with the current event or
+     * {@code -1} if no element is associated with the current event.
+     */
+    public int getAttributeCount() {
+        if (mCurrentEvent != EVENT_START_ELEMENT) {
+            return -1;
+        }
+
+        return mCurrentElementAttributeCount;
+    }
+
+    /**
+     * Returns the resource ID corresponding to the name of the specified attribute of the current
+     * element or {@code 0} if the name is not associated with a resource ID.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public int getAttributeNameResourceId(int index) throws XmlParserException {
+        return getAttribute(index).getNameResourceId();
+    }
+
+    /**
+     * Returns the name of the specified attribute of the current element.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public String getAttributeName(int index) throws XmlParserException {
+        return getAttribute(index).getName();
+    }
+
+    /**
+     * Returns the name of the specified attribute of the current element or an empty string if
+     * the attribute is not associated with a namespace.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public String getAttributeNamespace(int index) throws XmlParserException {
+        return getAttribute(index).getNamespace();
+    }
+
+    /**
+     * Returns the value type of the specified attribute of the current element. See
+     * {@code VALUE_TYPE_...} constants.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public int getAttributeValueType(int index) throws XmlParserException {
+        int type = getAttribute(index).getValueType();
+        switch (type) {
+            case Attribute.TYPE_STRING:
+                return VALUE_TYPE_STRING;
+            case Attribute.TYPE_INT_DEC:
+            case Attribute.TYPE_INT_HEX:
+                return VALUE_TYPE_INT;
+            case Attribute.TYPE_REFERENCE:
+                return VALUE_TYPE_REFERENCE;
+            case Attribute.TYPE_INT_BOOLEAN:
+                return VALUE_TYPE_BOOLEAN;
+            default:
+                return VALUE_TYPE_UNSUPPORTED;
+        }
+    }
+
+    /**
+     * Returns the integer value of the specified attribute of the current element. See
+     * {@code VALUE_TYPE_...} constants.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event.
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public int getAttributeIntValue(int index) throws XmlParserException {
+        return getAttribute(index).getIntValue();
+    }
+
+    /**
+     * Returns the boolean value of the specified attribute of the current element. See
+     * {@code VALUE_TYPE_...} constants.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event.
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public boolean getAttributeBooleanValue(int index) throws XmlParserException {
+        return getAttribute(index).getBooleanValue();
+    }
+
+    /**
+     * Returns the string value of the specified attribute of the current element. See
+     * {@code VALUE_TYPE_...} constants.
+     *
+     * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+     *         {@code start element} event.
+     * @throws XmlParserException if a parsing error is occurred
+     */
+    public String getAttributeStringValue(int index) throws XmlParserException {
+        return getAttribute(index).getStringValue();
+    }
+
+    private Attribute getAttribute(int index) {
+        if (mCurrentEvent != EVENT_START_ELEMENT) {
+            throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
+        }
+        if (index < 0) {
+            throw new IndexOutOfBoundsException("index must be >= 0");
+        }
+        if (index >= mCurrentElementAttributeCount) {
+            throw new IndexOutOfBoundsException(
+                    "index must be <= attr count (" + mCurrentElementAttributeCount + ")");
+        }
+        parseCurrentElementAttributesIfNotParsed();
+        return mCurrentElementAttributes.get(index);
+    }
+
+    /**
+     * Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
+     */
+    public int next() throws XmlParserException {
+        // Decrement depth if the previous event was "end element".
+        if (mCurrentEvent == EVENT_END_ELEMENT) {
+            mDepth--;
+        }
+
+        // Read events from document, ignoring events that we don't report to caller. Stop at the
+        // earliest event which we report to caller.
+        while (mXml.hasRemaining()) {
+            Chunk chunk = Chunk.get(mXml);
+            if (chunk == null) {
+                break;
+            }
+            switch (chunk.getType()) {
+                case Chunk.TYPE_STRING_POOL:
+                    if (mStringPool != null) {
+                        throw new XmlParserException("Multiple string pools not supported");
+                    }
+                    mStringPool = new StringPool(chunk);
+                    break;
+
+                case Chunk.RES_XML_TYPE_START_ELEMENT:
+                {
+                    if (mStringPool == null) {
+                        throw new XmlParserException(
+                                "Named element encountered before string pool");
+                    }
+                    ByteBuffer contents = chunk.getContents();
+                    if (contents.remaining() < 20) {
+                        throw new XmlParserException(
+                                "Start element chunk too short. Need at least 20 bytes. Available: "
+                                        + contents.remaining() + " bytes");
+                    }
+                    long nsId = getUnsignedInt32(contents);
+                    long nameId = getUnsignedInt32(contents);
+                    int attrStartOffset = getUnsignedInt16(contents);
+                    int attrSizeBytes = getUnsignedInt16(contents);
+                    int attrCount = getUnsignedInt16(contents);
+                    long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
+                    contents.position(0);
+                    if (attrStartOffset > contents.remaining()) {
+                        throw new XmlParserException(
+                                "Attributes start offset out of bounds: " + attrStartOffset
+                                    + ", max: " + contents.remaining());
+                    }
+                    if (attrEndOffset > contents.remaining()) {
+                        throw new XmlParserException(
+                                "Attributes end offset out of bounds: " + attrEndOffset
+                                    + ", max: " + contents.remaining());
+                    }
+
+                    mCurrentElementName = mStringPool.getString(nameId);
+                    mCurrentElementNamespace =
+                            (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
+                    mCurrentElementAttributeCount = attrCount;
+                    mCurrentElementAttributes = null;
+                    mCurrentElementAttrSizeBytes = attrSizeBytes;
+                    mCurrentElementAttributesContents =
+                            sliceFromTo(contents, attrStartOffset, attrEndOffset);
+
+                    mDepth++;
+                    mCurrentEvent = EVENT_START_ELEMENT;
+                    return mCurrentEvent;
+                }
+
+                case Chunk.RES_XML_TYPE_END_ELEMENT:
+                {
+                    if (mStringPool == null) {
+                        throw new XmlParserException(
+                                "Named element encountered before string pool");
+                    }
+                    ByteBuffer contents = chunk.getContents();
+                    if (contents.remaining() < 8) {
+                        throw new XmlParserException(
+                                "End element chunk too short. Need at least 8 bytes. Available: "
+                                        + contents.remaining() + " bytes");
+                    }
+                    long nsId = getUnsignedInt32(contents);
+                    long nameId = getUnsignedInt32(contents);
+                    mCurrentElementName = mStringPool.getString(nameId);
+                    mCurrentElementNamespace =
+                            (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
+                    mCurrentEvent = EVENT_END_ELEMENT;
+                    mCurrentElementAttributes = null;
+                    mCurrentElementAttributesContents = null;
+                    return mCurrentEvent;
+                }
+                case Chunk.RES_XML_TYPE_RESOURCE_MAP:
+                    if (mResourceMap != null) {
+                        throw new XmlParserException("Multiple resource maps not supported");
+                    }
+                    mResourceMap = new ResourceMap(chunk);
+                    break;
+                default:
+                    // Unknown chunk type -- ignore
+                    break;
+            }
+        }
+
+        mCurrentEvent = EVENT_END_DOCUMENT;
+        return mCurrentEvent;
+    }
+
+    private void parseCurrentElementAttributesIfNotParsed() {
+        if (mCurrentElementAttributes != null) {
+            return;
+        }
+        mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
+        for (int i = 0; i < mCurrentElementAttributeCount; i++) {
+            int startPosition = i * mCurrentElementAttrSizeBytes;
+            ByteBuffer attr =
+                    sliceFromTo(
+                            mCurrentElementAttributesContents,
+                            startPosition,
+                            startPosition + mCurrentElementAttrSizeBytes);
+            long nsId = getUnsignedInt32(attr);
+            long nameId = getUnsignedInt32(attr);
+            attr.position(attr.position() + 7); // skip ignored fields
+            int valueType = getUnsignedInt8(attr);
+            long valueData = getUnsignedInt32(attr);
+            mCurrentElementAttributes.add(
+                    new Attribute(
+                            nsId,
+                            nameId,
+                            valueType,
+                            (int) valueData,
+                            mStringPool,
+                            mResourceMap));
+        }
+    }
+
+    private static class Attribute {
+        private static final int TYPE_REFERENCE = 1;
+        private static final int TYPE_STRING = 3;
+        private static final int TYPE_INT_DEC = 0x10;
+        private static final int TYPE_INT_HEX = 0x11;
+        private static final int TYPE_INT_BOOLEAN = 0x12;
+
+        private final long mNsId;
+        private final long mNameId;
+        private final int mValueType;
+        private final int mValueData;
+        private final StringPool mStringPool;
+        private final ResourceMap mResourceMap;
+
+        private Attribute(
+                long nsId,
+                long nameId,
+                int valueType,
+                int valueData,
+                StringPool stringPool,
+                ResourceMap resourceMap) {
+            mNsId = nsId;
+            mNameId = nameId;
+            mValueType = valueType;
+            mValueData = valueData;
+            mStringPool = stringPool;
+            mResourceMap = resourceMap;
+        }
+
+        public int getNameResourceId() {
+            return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
+        }
+
+        public String getName() throws XmlParserException {
+            return mStringPool.getString(mNameId);
+        }
+
+        public String getNamespace() throws XmlParserException {
+            return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
+        }
+
+        public int getValueType() {
+            return mValueType;
+        }
+
+        public int getIntValue() throws XmlParserException {
+            switch (mValueType) {
+                case TYPE_REFERENCE:
+                case TYPE_INT_DEC:
+                case TYPE_INT_HEX:
+                case TYPE_INT_BOOLEAN:
+                    return mValueData;
+                default:
+                    throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
+            }
+        }
+
+        public boolean getBooleanValue() throws XmlParserException {
+            switch (mValueType) {
+                case TYPE_INT_BOOLEAN:
+                    return mValueData != 0;
+                default:
+                    throw new XmlParserException(
+                            "Cannot coerce to boolean: value type " + mValueType);
+            }
+        }
+
+        public String getStringValue() throws XmlParserException {
+            switch (mValueType) {
+                case TYPE_STRING:
+                    return mStringPool.getString(mValueData & 0xffffffffL);
+                case TYPE_INT_DEC:
+                    return Integer.toString(mValueData);
+                case TYPE_INT_HEX:
+                    return "0x" + Integer.toHexString(mValueData);
+                case TYPE_INT_BOOLEAN:
+                    return Boolean.toString(mValueData != 0);
+                case TYPE_REFERENCE:
+                    return "@" + Integer.toHexString(mValueData);
+                default:
+                    throw new XmlParserException(
+                            "Cannot coerce to string: value type " + mValueType);
+            }
+        }
+    }
+
+    /**
+     * Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
+     * contents.
+     */
+    private static class Chunk {
+        public static final int TYPE_STRING_POOL = 1;
+        public static final int TYPE_RES_XML = 3;
+        public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
+        public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
+        public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
+
+        static final int HEADER_MIN_SIZE_BYTES = 8;
+
+        private final int mType;
+        private final ByteBuffer mHeader;
+        private final ByteBuffer mContents;
+
+        public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
+            mType = type;
+            mHeader = header;
+            mContents = contents;
+        }
+
+        public ByteBuffer getContents() {
+            ByteBuffer result = mContents.slice();
+            result.order(mContents.order());
+            return result;
+        }
+
+        public ByteBuffer getHeader() {
+            ByteBuffer result = mHeader.slice();
+            result.order(mHeader.order());
+            return result;
+        }
+
+        public int getType() {
+            return mType;
+        }
+
+        /**
+         * Consumes the chunk located at the current position of the input and returns the chunk
+         * or {@code null} if there is no chunk left in the input.
+         *
+         * @throws XmlParserException if the chunk is malformed
+         */
+        public static Chunk get(ByteBuffer input) throws XmlParserException {
+            if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
+                // Android ignores the last chunk if its header is too big to fit into the file
+                input.position(input.limit());
+                return null;
+            }
+
+            int originalPosition = input.position();
+            int type = getUnsignedInt16(input);
+            int headerSize = getUnsignedInt16(input);
+            long chunkSize = getUnsignedInt32(input);
+            long chunkRemaining = chunkSize - 8;
+            if (chunkRemaining > input.remaining()) {
+                // Android ignores the last chunk if it's too big to fit into the file
+                input.position(input.limit());
+                return null;
+            }
+            if (headerSize < HEADER_MIN_SIZE_BYTES) {
+                throw new XmlParserException(
+                        "Malformed chunk: header too short: " + headerSize + " bytes");
+            } else if (headerSize > chunkSize) {
+                throw new XmlParserException(
+                        "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
+                                + chunkSize + " bytes");
+            }
+            int contentStartPosition = originalPosition + headerSize;
+            long chunkEndPosition = originalPosition + chunkSize;
+            Chunk chunk =
+                    new Chunk(
+                            type,
+                            sliceFromTo(input, originalPosition, contentStartPosition),
+                            sliceFromTo(input, contentStartPosition, chunkEndPosition));
+            input.position((int) chunkEndPosition);
+            return chunk;
+        }
+    }
+
+    /**
+     * String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
+     */
+    private static class StringPool {
+        private static final int FLAG_UTF8 = 1 << 8;
+
+        private final ByteBuffer mChunkContents;
+        private final ByteBuffer mStringsSection;
+        private final int mStringCount;
+        private final boolean mUtf8Encoded;
+        private final Map<Integer, String> mCachedStrings = new HashMap<>();
+
+        /**
+         * Constructs a new string pool from the provided chunk.
+         *
+         * @throws XmlParserException if a parsing error occurred
+         */
+        public StringPool(Chunk chunk) throws XmlParserException {
+            ByteBuffer header = chunk.getHeader();
+            int headerSizeBytes = header.remaining();
+            header.position(Chunk.HEADER_MIN_SIZE_BYTES);
+            if (header.remaining() < 20) {
+                throw new XmlParserException(
+                        "XML chunk's header too short. Required at least 20 bytes. Available: "
+                                + header.remaining() + " bytes");
+            }
+            long stringCount = getUnsignedInt32(header);
+            if (stringCount > Integer.MAX_VALUE) {
+                throw new XmlParserException("Too many strings: " + stringCount);
+            }
+            mStringCount = (int) stringCount;
+            long styleCount = getUnsignedInt32(header);
+            if (styleCount > Integer.MAX_VALUE) {
+                throw new XmlParserException("Too many styles: " + styleCount);
+            }
+            long flags = getUnsignedInt32(header);
+            long stringsStartOffset = getUnsignedInt32(header);
+            long stylesStartOffset = getUnsignedInt32(header);
+
+            ByteBuffer contents = chunk.getContents();
+            if (mStringCount > 0) {
+                int stringsSectionStartOffsetInContents =
+                        (int) (stringsStartOffset - headerSizeBytes);
+                int stringsSectionEndOffsetInContents;
+                if (styleCount > 0) {
+                    // Styles section follows the strings section
+                    if (stylesStartOffset < stringsStartOffset) {
+                        throw new XmlParserException(
+                                "Styles offset (" + stylesStartOffset + ") < strings offset ("
+                                        + stringsStartOffset + ")");
+                    }
+                    stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
+                } else {
+                    stringsSectionEndOffsetInContents = contents.remaining();
+                }
+                mStringsSection =
+                        sliceFromTo(
+                                contents,
+                                stringsSectionStartOffsetInContents,
+                                stringsSectionEndOffsetInContents);
+            } else {
+                mStringsSection = ByteBuffer.allocate(0);
+            }
+
+            mUtf8Encoded = (flags & FLAG_UTF8) != 0;
+            mChunkContents = contents;
+        }
+
+        /**
+         * Returns the string located at the specified {@code 0}-based index in this pool.
+         *
+         * @throws XmlParserException if the string does not exist or cannot be decoded
+         */
+        public String getString(long index) throws XmlParserException {
+            if (index < 0) {
+                throw new XmlParserException("Unsuported string index: " + index);
+            } else if (index >= mStringCount) {
+                throw new XmlParserException(
+                        "Unsuported string index: " + index + ", max: " + (mStringCount - 1));
+            }
+
+            int idx = (int) index;
+            String result = mCachedStrings.get(idx);
+            if (result != null) {
+                return result;
+            }
+
+            long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
+            if (offsetInStringsSection >= mStringsSection.capacity()) {
+                throw new XmlParserException(
+                        "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
+                                + ", max: " + (mStringsSection.capacity() - 1));
+            }
+            mStringsSection.position((int) offsetInStringsSection);
+            result =
+                    (mUtf8Encoded)
+                            ? getLengthPrefixedUtf8EncodedString(mStringsSection)
+                            : getLengthPrefixedUtf16EncodedString(mStringsSection);
+            mCachedStrings.put(idx, result);
+            return result;
+        }
+
+        private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
+                throws XmlParserException {
+            // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
+            // Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
+            // of supported values is 0 to 0x7fffffff inclusive.
+            int lengthChars = getUnsignedInt16(encoded);
+            if ((lengthChars & 0x8000) != 0) {
+                lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
+            }
+            if (lengthChars > Integer.MAX_VALUE / 2) {
+                throw new XmlParserException("String too long: " + lengthChars + " uint16s");
+            }
+            int lengthBytes = lengthChars * 2;
+
+            byte[] arr;
+            int arrOffset;
+            if (encoded.hasArray()) {
+                arr = encoded.array();
+                arrOffset = encoded.arrayOffset() + encoded.position();
+                encoded.position(encoded.position() + lengthBytes);
+            } else {
+                arr = new byte[lengthBytes];
+                arrOffset = 0;
+                encoded.get(arr);
+            }
+            // Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
+            // array of bytes is NULL terminated.
+            if ((arr[arrOffset + lengthBytes] != 0)
+                    || (arr[arrOffset + lengthBytes + 1] != 0)) {
+                throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
+            }
+            try {
+                return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException("UTF-16LE character encoding not supported", e);
+            }
+        }
+
+        private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
+                throws XmlParserException {
+            // If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
+            // it is stored as a big-endian uint16 with highest bit set. Thus, the range of
+            // supported values is 0 to 0x7fff inclusive.
+
+            // Skip UTF-16 encoded length (in uint16s)
+            int lengthBytes = getUnsignedInt8(encoded);
+            if ((lengthBytes & 0x80) != 0) {
+                lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
+            }
+
+            // Read UTF-8 encoded length (in bytes)
+            lengthBytes = getUnsignedInt8(encoded);
+            if ((lengthBytes & 0x80) != 0) {
+                lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
+            }
+
+            byte[] arr;
+            int arrOffset;
+            if (encoded.hasArray()) {
+                arr = encoded.array();
+                arrOffset = encoded.arrayOffset() + encoded.position();
+                encoded.position(encoded.position() + lengthBytes);
+            } else {
+                arr = new byte[lengthBytes];
+                arrOffset = 0;
+                encoded.get(arr);
+            }
+            // Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
+            // of bytes is NULL terminated.
+            if (arr[arrOffset + lengthBytes] != 0) {
+                throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
+            }
+            try {
+                return new String(arr, arrOffset, lengthBytes, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException("UTF-8 character encoding not supported", e);
+            }
+        }
+    }
+
+    /**
+     * Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
+     * map.
+     */
+    private static class ResourceMap {
+        private final ByteBuffer mChunkContents;
+        private final int mEntryCount;
+
+        /**
+         * Constructs a new resource map from the provided chunk.
+         *
+         * @throws XmlParserException if a parsing error occurred
+         */
+        public ResourceMap(Chunk chunk) throws XmlParserException {
+            mChunkContents = chunk.getContents().slice();
+            mChunkContents.order(chunk.getContents().order());
+            // Each entry of the map is four bytes long, containing the int32 resource ID.
+            mEntryCount = mChunkContents.remaining() /  4;
+        }
+
+        /**
+         * Returns the resource ID located at the specified {@code 0}-based index in this pool or
+         * {@code 0} if the index is out of range.
+         */
+        public int getResourceId(long index) {
+            if ((index < 0) || (index >= mEntryCount)) {
+                return 0;
+            }
+            int idx = (int) index;
+            // Each entry of the map is four bytes long, containing the int32 resource ID.
+            return mChunkContents.getInt(idx * 4);
+        }
+    }
+
+    /**
+     * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+     * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+     * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+     * buffer's byte order.
+     */
+    private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
+        if (start < 0) {
+            throw new IllegalArgumentException("start: " + start);
+        }
+        if (end < start) {
+            throw new IllegalArgumentException("end < start: " + end + " < " + start);
+        }
+        int capacity = source.capacity();
+        if (end > source.capacity()) {
+            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+        }
+        return sliceFromTo(source, (int) start, (int) end);
+    }
+
+    /**
+     * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+     * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+     * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+     * buffer's byte order.
+     */
+    private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+        if (start < 0) {
+            throw new IllegalArgumentException("start: " + start);
+        }
+        if (end < start) {
+            throw new IllegalArgumentException("end < start: " + end + " < " + start);
+        }
+        int capacity = source.capacity();
+        if (end > source.capacity()) {
+            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+        }
+        int originalLimit = source.limit();
+        int originalPosition = source.position();
+        try {
+            source.position(0);
+            source.limit(end);
+            source.position(start);
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            return result;
+        } finally {
+            source.position(0);
+            source.limit(originalLimit);
+            source.position(originalPosition);
+        }
+    }
+
+    private static int getUnsignedInt8(ByteBuffer buffer) {
+        return buffer.get() & 0xff;
+    }
+
+    private static int getUnsignedInt16(ByteBuffer buffer) {
+        return buffer.getShort() & 0xffff;
+    }
+
+    private static long getUnsignedInt32(ByteBuffer buffer) {
+        return buffer.getInt() & 0xffffffffL;
+    }
+
+    private static long getUnsignedInt32(ByteBuffer buffer, int position) {
+        return buffer.getInt(position) & 0xffffffffL;
+    }
+
+    /**
+     * Indicates that an error occurred while parsing a document.
+     */
+    public static class XmlParserException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public XmlParserException(String message) {
+            super(message);
+        }
+
+        public XmlParserException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+}

+ 104 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 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.apk;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signature verification result.
+ */
+public class ApkSigResult {
+    public final int signatureSchemeVersion;
+
+    /** Whether the APK's Signature Scheme signature verifies. */
+    public boolean verified;
+
+    public final List<ApkSignerInfo> mSigners = new ArrayList<>();
+    private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+    private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+    public ApkSigResult(int signatureSchemeVersion) {
+        this.signatureSchemeVersion = signatureSchemeVersion;
+    }
+
+    /**
+     * Returns {@code true} if this result encountered errors during verification.
+     */
+    public boolean containsErrors() {
+        if (!mErrors.isEmpty()) {
+            return true;
+        }
+        if (!mSigners.isEmpty()) {
+            for (ApkSignerInfo signer : mSigners) {
+                if (signer.containsErrors()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if this result encountered warnings during verification.
+     */
+    public boolean containsWarnings() {
+        if (!mWarnings.isEmpty()) {
+            return true;
+        }
+        if (!mSigners.isEmpty()) {
+            for (ApkSignerInfo signer : mSigners) {
+                if (signer.containsWarnings()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addError(int issueId, Object... parameters) {
+        mErrors.add(new ApkVerificationIssue(issueId, parameters));
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addWarning(int issueId, Object... parameters) {
+        mWarnings.add(new ApkVerificationIssue(issueId, parameters));
+    }
+
+    /**
+     * Returns the errors encountered during verification.
+     */
+    public List<? extends ApkVerificationIssue> getErrors() {
+        return mErrors;
+    }
+
+    /**
+     * Returns the warnings encountered during verification.
+     */
+    public List<? extends ApkVerificationIssue> getWarnings() {
+        return mWarnings;
+    }
+}

+ 104 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 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.apk;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signer.
+ */
+public class ApkSignerInfo {
+    public int index;
+    public long timestamp;
+    public List<X509Certificate> certs = new ArrayList<>();
+    public List<X509Certificate> certificateLineage = new ArrayList<>();
+
+    private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
+    private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+    private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addError(int issueId, Object... params) {
+        mErrors.add(new ApkVerificationIssue(issueId, params));
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addWarning(int issueId, Object... params) {
+        mWarnings.add(new ApkVerificationIssue(issueId, params));
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the
+     * provided {@code issueId} and {@code params}.
+     */
+    public void addInfoMessage(int issueId, Object... params) {
+        mInfoMessages.add(new ApkVerificationIssue(issueId, params));
+    }
+
+    /**
+     * Returns {@code true} if any errors were encountered during verification for this signer.
+     */
+    public boolean containsErrors() {
+        return !mErrors.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} if any warnings were encountered during verification for this signer.
+     */
+    public boolean containsWarnings() {
+        return !mWarnings.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} if any info messages were encountered during verification of this
+     * signer.
+     */
+    public boolean containsInfoMessages() {
+        return !mInfoMessages.isEmpty();
+    }
+
+    /**
+     * Returns the errors encountered during verification for this signer.
+     */
+    public List<? extends ApkVerificationIssue> getErrors() {
+        return mErrors;
+    }
+
+    /**
+     * Returns the warnings encountered during verification for this signer.
+     */
+    public List<? extends ApkVerificationIssue> getWarnings() {
+        return mWarnings;
+    }
+
+    /**
+     * Returns the info messages encountered during verification of this signer.
+     */
+    public List<? extends ApkVerificationIssue> getInfoMessages() {
+        return mInfoMessages;
+    }
+}

+ 1444 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java

@@ -0,0 +1,1444 @@
+/*
+ * 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.apk;
+
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512;
+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;
+import com.android.apksig.apk.ApkUtils;
+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.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+import com.android.apksig.internal.pkcs7.SignedData;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.pkcs7.SignerInfo;
+import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.util.ChainedDataSource;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.VerityTreeBuilder;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.internal.x509.RSAPublicKey;
+import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import javax.security.auth.x500.X500Principal;
+
+public class ApkSigningBlockUtils {
+
+    private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
+    public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
+    private static final byte[] APK_SIGNING_BLOCK_MAGIC =
+          new byte[] {
+              0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
+              0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
+          };
+    public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
+
+    private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
+            {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};
+
+    public static final int VERSION_SOURCE_STAMP = 0;
+    public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+    /**
+     * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+     * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+     */
+    public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+        return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2);
+    }
+
+    /**
+     * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
+     * APK and comparing them against the digests listed in APK Signing Block. The expected digests
+     * are taken from {@code SignerInfos} of the provided {@code result}.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on Android. No errors are added to the {@code result} if the APK's
+     * integrity is expected to verify on Android for each algorithm in
+     * {@code contentDigestAlgorithms}.
+     *
+     * <p>The reason this method is currently not parameterized by a
+     * {@code [minSdkVersion, maxSdkVersion]} range is that up until now content digest algorithms
+     * exhibit the same behavior on all Android platform versions.
+     */
+    public static void verifyIntegrity(
+            RunnablesExecutor executor,
+            DataSource beforeApkSigningBlock,
+            DataSource centralDir,
+            ByteBuffer eocd,
+            Set<ContentDigestAlgorithm> contentDigestAlgorithms,
+            Result result) throws IOException, NoSuchAlgorithmException {
+        if (contentDigestAlgorithms.isEmpty()) {
+            // This should never occur because this method is invoked once at least one signature
+            // is verified, meaning at least one content digest is known.
+            throw new RuntimeException("No content digests found");
+        }
+
+        // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
+        // treated as though its Central Directory offset points to the start of APK Signing Block.
+        // We thus modify the EoCD accordingly.
+        ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
+        int eocdSavedPos = eocd.position();
+        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
+        modifiedEocd.put(eocd);
+        modifiedEocd.flip();
+
+        // restore eocd to position prior to modification in case it is to be used elsewhere
+        eocd.position(eocdSavedPos);
+        ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
+        Map<ContentDigestAlgorithm, byte[]> actualContentDigests;
+        try {
+            actualContentDigests =
+                    computeContentDigests(
+                            executor,
+                            contentDigestAlgorithms,
+                            beforeApkSigningBlock,
+                            centralDir,
+                            new ByteBufferDataSource(modifiedEocd));
+            // Special checks for the verity algorithm requirements.
+            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: " +
+                            beforeApkSigningBlock.size());
+                }
+
+                long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
+                long signingBlockSize = centralDirOffset - beforeApkSigningBlock.size();
+                if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
+                    throw new RuntimeException(
+                            "APK Signing Block size is not multiple of page size: " +
+                            signingBlockSize);
+                }
+            }
+        } catch (DigestException e) {
+            throw new RuntimeException("Failed to compute content digests", e);
+        }
+        if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
+            throw new RuntimeException(
+                    "Mismatch between sets of requested and computed content digests"
+                            + " . Requested: " + contentDigestAlgorithms
+                            + ", computed: " + actualContentDigests.keySet());
+        }
+
+        // Compare digests computed over the rest of APK against the corresponding expected digests
+        // in signer blocks.
+        for (Result.SignerInfo signerInfo : result.signers) {
+            for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
+                SignatureAlgorithm signatureAlgorithm =
+                        SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                ContentDigestAlgorithm contentDigestAlgorithm =
+                        signatureAlgorithm.getContentDigestAlgorithm();
+                // if the current digest algorithm is not in the list provided by the caller then
+                // ignore it; the signer may contain digests not recognized by the specified SDK
+                // range.
+                if (!contentDigestAlgorithms.contains(contentDigestAlgorithm)) {
+                    continue;
+                }
+                byte[] expectedDigest = expected.getValue();
+                byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
+                if (!Arrays.equals(expectedDigest, actualDigest)) {
+                    if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+                        signerInfo.addError(
+                                ApkVerifier.Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
+                                contentDigestAlgorithm,
+                                toHex(expectedDigest),
+                                toHex(actualDigest));
+                    } else if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3) {
+                        signerInfo.addError(
+                                ApkVerifier.Issue.V3_SIG_APK_DIGEST_DID_NOT_VERIFY,
+                                contentDigestAlgorithm,
+                                toHex(expectedDigest),
+                                toHex(actualDigest));
+                    }
+                    continue;
+                }
+                signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
+            }
+        }
+    }
+
+    public static ByteBuffer findApkSignatureSchemeBlock(
+            ByteBuffer apkSigningBlock,
+            int blockId,
+            Result result) throws SignatureNotFoundException {
+        try {
+            return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId);
+        } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+            throw new SignatureNotFoundException(e.getMessage());
+        }
+    }
+
+    public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+        ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer);
+    }
+
+    public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+        return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source);
+    }
+
+    public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+        return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf);
+    }
+
+    public static String toHex(byte[] value) {
+        return ApkSigningBlockUtilsLite.toHex(value);
+    }
+
+    public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+            RunnablesExecutor executor,
+            Set<ContentDigestAlgorithm> digestAlgorithms,
+            DataSource beforeCentralDir,
+            DataSource centralDir,
+            DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException {
+        Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>();
+        Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>();
+        for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) {
+            if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+                    || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
+                oneMbChunkBasedAlgorithm.add(digestAlgorithm);
+            }
+        }
+        computeOneMbChunkContentDigests(
+                executor,
+                oneMbChunkBasedAlgorithm,
+                new DataSource[] { beforeCentralDir, centralDir, eocd },
+                contentDigests);
+
+        if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
+            computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
+        }
+        return contentDigests;
+    }
+
+    static void computeOneMbChunkContentDigests(
+            Set<ContentDigestAlgorithm> digestAlgorithms,
+            DataSource[] contents,
+            Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
+            throws IOException, NoSuchAlgorithmException, DigestException {
+        // For each digest algorithm the result is computed as follows:
+        // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
+        //    The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
+        //    No chunks are produced for empty (zero length) segments.
+        // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
+        //    length in bytes (uint32 little-endian) and the chunk's contents.
+        // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
+        //    chunks (uint32 little-endian) and the concatenation of digests of chunks of all
+        //    segments in-order.
+
+        long chunkCountLong = 0;
+        for (DataSource input : contents) {
+            chunkCountLong +=
+                    getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+        }
+        if (chunkCountLong > Integer.MAX_VALUE) {
+            throw new DigestException("Input too long: " + chunkCountLong + " chunks");
+        }
+        int chunkCount = (int) chunkCountLong;
+
+        ContentDigestAlgorithm[] digestAlgorithmsArray =
+                digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
+        MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
+        byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
+        int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
+        for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+            ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
+            int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
+            digestOutputSizes[i] = digestOutputSizeBytes;
+            byte[] concatenationOfChunkCountAndChunkDigests =
+                    new byte[5 + chunkCount * digestOutputSizeBytes];
+            concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
+            setUnsignedInt32LittleEndian(
+                    chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
+            digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
+            String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+            mds[i] = MessageDigest.getInstance(jcaAlgorithm);
+        }
+
+        DataSink mdSink = DataSinks.asDataSink(mds);
+        byte[] chunkContentPrefix = new byte[5];
+        chunkContentPrefix[0] = (byte) 0xa5;
+        int chunkIndex = 0;
+        // Optimization opportunity: digests of chunks can be computed in parallel. However,
+        // determining the number of computations to be performed in parallel is non-trivial. This
+        // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
+        // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
+        // cores, load on the system from other threads of execution and other processes, size of
+        // input.
+        // For now, we compute these digests sequentially and thus have the luxury of improving
+        // performance by writing the digest of each chunk into a pre-allocated buffer at exactly
+        // the right position. This avoids unnecessary allocations, copying, and enables the final
+        // digest to be more efficient because it's presented with all of its input in one go.
+        for (DataSource input : contents) {
+            long inputOffset = 0;
+            long inputRemaining = input.size();
+            while (inputRemaining > 0) {
+                int chunkSize =
+                        (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+                setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
+                for (int i = 0; i < mds.length; i++) {
+                    mds[i].update(chunkContentPrefix);
+                }
+                try {
+                    input.feed(inputOffset, chunkSize, mdSink);
+                } catch (IOException e) {
+                    throw new IOException("Failed to read chunk #" + chunkIndex, e);
+                }
+                for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+                    MessageDigest md = mds[i];
+                    byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+                    int expectedDigestSizeBytes = digestOutputSizes[i];
+                    int actualDigestSizeBytes =
+                            md.digest(
+                                    concatenationOfChunkCountAndChunkDigests,
+                                    5 + chunkIndex * expectedDigestSizeBytes,
+                                    expectedDigestSizeBytes);
+                    if (actualDigestSizeBytes != expectedDigestSizeBytes) {
+                        throw new RuntimeException(
+                                "Unexpected output size of " + md.getAlgorithm()
+                                        + " digest: " + actualDigestSizeBytes);
+                    }
+                }
+                inputOffset += chunkSize;
+                inputRemaining -= chunkSize;
+                chunkIndex++;
+            }
+        }
+
+        for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+            ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
+            byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+            MessageDigest md = mds[i];
+            byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
+            outputContentDigests.put(digestAlgorithm, digest);
+        }
+    }
+
+    static void computeOneMbChunkContentDigests(
+            RunnablesExecutor executor,
+            Set<ContentDigestAlgorithm> digestAlgorithms,
+            DataSource[] contents,
+            Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
+            throws NoSuchAlgorithmException, DigestException {
+        long chunkCountLong = 0;
+        for (DataSource input : contents) {
+            chunkCountLong +=
+                    getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+        }
+        if (chunkCountLong > Integer.MAX_VALUE) {
+            throw new DigestException("Input too long: " + chunkCountLong + " chunks");
+        }
+        int chunkCount = (int) chunkCountLong;
+
+        List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
+        for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
+            chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
+        }
+
+        ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
+        executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList));
+
+        // Compute and write out final digest for each algorithm.
+        for (ChunkDigests chunkDigests : chunkDigestsList) {
+            MessageDigest messageDigest = chunkDigests.createMessageDigest();
+            outputContentDigests.put(
+                    chunkDigests.algorithm,
+                    messageDigest.digest(chunkDigests.concatOfDigestsOfChunks));
+        }
+    }
+
+    private static class ChunkDigests {
+        private final ContentDigestAlgorithm algorithm;
+        private final int digestOutputSize;
+        private final byte[] concatOfDigestsOfChunks;
+
+        private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) {
+            this.algorithm = algorithm;
+            digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes();
+            concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize];
+
+            // Fill the initial values of the concatenated digests of chunks, which is
+            // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
+            concatOfDigestsOfChunks[0] = 0x5a;
+            setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1);
+        }
+
+        private MessageDigest createMessageDigest() throws NoSuchAlgorithmException {
+            return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm());
+        }
+
+        private int getOffset(int chunkIndex) {
+            return 1 + 4 + chunkIndex * digestOutputSize;
+        }
+    }
+
+    /**
+     * A per-thread digest worker.
+     */
+    private static class ChunkDigester implements Runnable {
+        private final ChunkSupplier dataSupplier;
+        private final List<ChunkDigests> chunkDigests;
+        private final List<MessageDigest> messageDigests;
+        private final DataSink mdSink;
+
+        private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests) {
+            this.dataSupplier = dataSupplier;
+            this.chunkDigests = chunkDigests;
+            messageDigests = new ArrayList<>(chunkDigests.size());
+            for (ChunkDigests chunkDigest : chunkDigests) {
+                try {
+                    messageDigests.add(chunkDigest.createMessageDigest());
+                } catch (NoSuchAlgorithmException ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+            mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0]));
+        }
+
+        @Override
+        public void run() {
+            byte[] chunkContentPrefix = new byte[5];
+            chunkContentPrefix[0] = (byte) 0xa5;
+
+            try {
+                for (ChunkSupplier.Chunk chunk = dataSupplier.get();
+                     chunk != null;
+                     chunk = dataSupplier.get()) {
+                    int size = chunk.size;
+                    if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
+                        throw new RuntimeException("Chunk size greater than expected: " + size);
+                    }
+
+                    // First update with the chunk prefix.
+                    setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1);
+                    mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
+
+                    // Then update with the chunk data.
+                    mdSink.consume(chunk.data);
+
+                    // Now finalize chunk for all algorithms.
+                    for (int i = 0; i < chunkDigests.size(); i++) {
+                        ChunkDigests chunkDigest = chunkDigests.get(i);
+                        int actualDigestSize = messageDigests.get(i).digest(
+                                chunkDigest.concatOfDigestsOfChunks,
+                                chunkDigest.getOffset(chunk.chunkIndex),
+                                chunkDigest.digestOutputSize);
+                        if (actualDigestSize != chunkDigest.digestOutputSize) {
+                            throw new RuntimeException(
+                                    "Unexpected output size of " + chunkDigest.algorithm
+                                            + " digest: " + actualDigestSize);
+                        }
+                    }
+                }
+            } catch (IOException | DigestException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    /**
+     * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a
+     * supplied {@link DataSource}, the data from the next {@link DataSource}
+     * are NOT concatenated. Only the next call to get() will fetch from the
+     * next {@link DataSource} in the input {@link DataSource} array.
+     */
+    private static class ChunkSupplier implements Supplier<ChunkSupplier.Chunk> {
+        private final DataSource[] dataSources;
+        private final int[] chunkCounts;
+        private final int totalChunkCount;
+        private final AtomicInteger nextIndex;
+
+        private ChunkSupplier(DataSource[] dataSources) {
+            this.dataSources = dataSources;
+            chunkCounts = new int[dataSources.length];
+            int totalChunkCount = 0;
+            for (int i = 0; i < dataSources.length; i++) {
+                long chunkCount = getChunkCount(dataSources[i].size(),
+                        CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+                if (chunkCount > Integer.MAX_VALUE) {
+                    throw new RuntimeException(
+                            String.format(
+                                    "Number of chunks in dataSource[%d] is greater than max int.",
+                                    i));
+                }
+                chunkCounts[i] = (int)chunkCount;
+                totalChunkCount = (int) (totalChunkCount + chunkCount);
+            }
+            this.totalChunkCount = totalChunkCount;
+            nextIndex = new AtomicInteger(0);
+        }
+
+        /**
+         * We map an integer index to the termination-adjusted dataSources 1MB chunks.
+         * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned
+         * blocks in each input {@link DataSource} (unless the DataSource itself is
+         * 1MB-aligned).
+         */
+        @Override
+        public ChunkSupplier.Chunk get() {
+            int index = nextIndex.getAndIncrement();
+            if (index < 0 || index >= totalChunkCount) {
+                return null;
+            }
+
+            int dataSourceIndex = 0;
+            long dataSourceChunkOffset = index;
+            for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
+                if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
+                    break;
+                }
+                dataSourceChunkOffset -= chunkCounts[dataSourceIndex];
+            }
+
+            long remainingSize = Math.min(
+                    dataSources[dataSourceIndex].size() -
+                            dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
+                    CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+
+            final int size = (int)remainingSize;
+            final ByteBuffer buffer = ByteBuffer.allocate(size);
+            try {
+                dataSources[dataSourceIndex].copyTo(
+                        dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size,
+                        buffer);
+            } catch (IOException e) {
+                throw new IllegalStateException("Failed to read chunk", e);
+            }
+            buffer.rewind();
+
+            return new Chunk(index, buffer, size);
+        }
+
+        static class Chunk {
+            private final int chunkIndex;
+            private final ByteBuffer data;
+            private final int size;
+
+            private Chunk(int chunkIndex, ByteBuffer data, int size) {
+                this.chunkIndex = chunkIndex;
+                this.data = data;
+                this.size = size;
+            }
+        }
+    }
+
+    @SuppressWarnings("ByteBufferBackingArray")
+    private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
+            DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
+            throws IOException, NoSuchAlgorithmException {
+        ByteBuffer encoded = createVerityDigestBuffer(true);
+        // Use 0s as salt for now.  This also needs to be consistent in the fsverify header for
+        // kernel to use.
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8])) {
+            byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir,
+                    eocd);
+            encoded.put(rootHash);
+            encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
+            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      (optional) Length of source data
+        int backBufferSize =
+                VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
+        if (includeSourceDataSize) {
+            backBufferSize += Long.SIZE / Byte.SIZE;
+        }
+        ByteBuffer encoded = ByteBuffer.allocate(backBufferSize);
+        encoded.order(ByteOrder.LITTLE_ENDIAN);
+        return encoded;
+    }
+
+    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;
+        }
+    }
+
+    @SuppressWarnings("ByteBufferBackingArray")
+    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.
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
+            ByteBuffer tree = builder.generateVerityTree(dataSource);
+            byte[] rootHash = builder.getRootHashFromTree(tree);
+            encoded.put(rootHash);
+            return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array());
+        }
+    }
+
+    private static long getChunkCount(long inputSize, long chunkSize) {
+        return (inputSize + chunkSize - 1) / chunkSize;
+    }
+
+    private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
+        result[offset] = (byte) (value & 0xff);
+        result[offset + 1] = (byte) ((value >> 8) & 0xff);
+        result[offset + 2] = (byte) ((value >> 16) & 0xff);
+        result[offset + 3] = (byte) ((value >> 24) & 0xff);
+    }
+
+    public static byte[] encodePublicKey(PublicKey publicKey)
+            throws InvalidKeyException, NoSuchAlgorithmException {
+        byte[] encodedPublicKey = null;
+        if ("X.509".equals(publicKey.getFormat())) {
+            encodedPublicKey = publicKey.getEncoded();
+            // if the key is an RSA key check for a negative modulus
+            String keyAlgorithm = publicKey.getAlgorithm();
+            if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
+                try {
+                    // Parse the encoded public key into the separate elements of the
+                    // SubjectPublicKeyInfo to obtain the SubjectPublicKey.
+                    ByteBuffer encodedPublicKeyBuffer = ByteBuffer.wrap(encodedPublicKey);
+                    SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse(
+                            encodedPublicKeyBuffer, SubjectPublicKeyInfo.class);
+                    // The SubjectPublicKey is encoded as a bit string within the
+                    // SubjectPublicKeyInfo. The first byte of the encoding is the number of padding
+                    // bits; store this and decode the rest of the bit string into the RSA modulus
+                    // and exponent.
+                    ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey;
+                    byte padding = subjectPublicKeyBuffer.get();
+                    RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer,
+                            RSAPublicKey.class);
+                    // if the modulus is negative then attempt to reencode it with a leading 0 sign
+                    // byte.
+                    if (rsaPublicKey.modulus.compareTo(BigInteger.ZERO) < 0) {
+                        // A negative modulus indicates the leading bit in the integer is 1. Per
+                        // ASN.1 encoding rules to encode a positive integer with the leading bit
+                        // set to 1 a byte containing all zeros should precede the integer encoding.
+                        byte[] encodedModulus = rsaPublicKey.modulus.toByteArray();
+                        byte[] reencodedModulus = new byte[encodedModulus.length + 1];
+                        reencodedModulus[0] = 0;
+                        System.arraycopy(encodedModulus, 0, reencodedModulus, 1,
+                                encodedModulus.length);
+                        rsaPublicKey.modulus = new BigInteger(reencodedModulus);
+                        // Once the modulus has been corrected reencode the RSAPublicKey, then
+                        // restore the padding value in the bit string and reencode the entire
+                        // SubjectPublicKeyInfo to be returned to the caller.
+                        byte[] reencodedRSAPublicKey = Asn1DerEncoder.encode(rsaPublicKey);
+                        byte[] reencodedSubjectPublicKey =
+                                new byte[reencodedRSAPublicKey.length + 1];
+                        reencodedSubjectPublicKey[0] = padding;
+                        System.arraycopy(reencodedRSAPublicKey, 0, reencodedSubjectPublicKey, 1,
+                                reencodedRSAPublicKey.length);
+                        subjectPublicKeyInfo.subjectPublicKey = ByteBuffer.wrap(
+                                reencodedSubjectPublicKey);
+                        encodedPublicKey = Asn1DerEncoder.encode(subjectPublicKeyInfo);
+                    }
+                } catch (Asn1DecodingException | Asn1EncodingException e) {
+                    System.out.println("Caught a exception encoding the public key: " + e);
+                    e.printStackTrace();
+                    encodedPublicKey = null;
+                }
+            }
+        }
+        if (encodedPublicKey == null) {
+            try {
+                encodedPublicKey =
+                        KeyFactory.getInstance(publicKey.getAlgorithm())
+                                .getKeySpec(publicKey, X509EncodedKeySpec.class)
+                                .getEncoded();
+            } catch (InvalidKeySpecException e) {
+                throw new InvalidKeyException(
+                        "Failed to obtain X.509 encoded form of public key " + publicKey
+                                + " of class " + publicKey.getClass().getName(),
+                        e);
+            }
+        }
+        if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
+            throw new InvalidKeyException(
+                    "Failed to obtain X.509 encoded form of public key " + publicKey
+                            + " of class " + publicKey.getClass().getName());
+        }
+        return encodedPublicKey;
+    }
+
+    public static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
+            throws CertificateEncodingException {
+        List<byte[]> result = new ArrayList<>(certificates.size());
+        for (X509Certificate certificate : certificates) {
+            result.add(certificate.getEncoded());
+        }
+        return result;
+    }
+
+    public static byte[] encodeAsLengthPrefixedElement(byte[] bytes) {
+        byte[][] adapterBytes = new byte[1][];
+        adapterBytes[0] = bytes;
+        return encodeAsSequenceOfLengthPrefixedElements(adapterBytes);
+    }
+
+    public static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
+        return encodeAsSequenceOfLengthPrefixedElements(
+                sequence.toArray(new byte[sequence.size()][]));
+    }
+
+    public static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
+        int payloadSize = 0;
+        for (byte[] element : sequence) {
+            payloadSize += 4 + element.length;
+        }
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        for (byte[] element : sequence) {
+            result.putInt(element.length);
+            result.put(element);
+        }
+        return result.array();
+      }
+
+    public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+            List<Pair<Integer, byte[]>> sequence) {
+        return ApkSigningBlockUtilsLite
+                .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence);
+      }
+
+    /**
+     * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
+     * and the additional information relevant for verifying the block against the file.
+     *
+     * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+     *                identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+     *                block ID.
+     *
+     * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
+     * @throws IOException if an I/O error occurs while reading the APK
+     */
+    public static SignatureInfo findSignature(
+            DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result)
+                    throws IOException, SignatureNotFoundException {
+        try {
+            return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId);
+        } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+            throw new SignatureNotFoundException(e.getMessage());
+        }
+    }
+
+    /**
+     * Generates a new DataSource representing the APK contents before the Central Directory with
+     * padding, if padding is requested.  If the existing data entries before the Central Directory
+     * are already aligned, or no padding is requested, the original DataSource is used.  This
+     * padding is used to allow for verity-based APK verification.
+     *
+     * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of
+     *         padding used.
+     */
+    public static Pair<DataSource, Integer> generateApkSigningBlockPadding(
+            DataSource beforeCentralDir,
+            boolean apkSigningBlockPaddingSupported) {
+
+        // Ensure APK Signing Block starts from page boundary.
+        int padSizeBeforeSigningBlock = 0;
+        if (apkSigningBlockPaddingSupported &&
+                (beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
+            padSizeBeforeSigningBlock = (int) (
+                    ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
+                            beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
+            beforeCentralDir = new ChainedDataSource(
+                    beforeCentralDir,
+                    DataSources.asDataSource(
+                            ByteBuffer.allocate(padSizeBeforeSigningBlock)));
+        }
+        return Pair.of(beforeCentralDir, padSizeBeforeSigningBlock);
+    }
+
+    public static DataSource copyWithModifiedCDOffset(
+            DataSource beforeCentralDir, DataSource eocd) throws IOException {
+
+        // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
+        // offset field is treated as pointing to the offset at which the APK Signing Block will
+        // start.
+        long centralDirOffsetForDigesting = beforeCentralDir.size();
+        ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        eocd.copyTo(0, (int) eocd.size(), eocdBuf);
+        eocdBuf.flip();
+        ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
+        return DataSources.asDataSource(eocdBuf);
+    }
+
+    public static byte[] generateApkSigningBlock(
+            List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) {
+        // FORMAT:
+        // uint64:  size (excluding this field)
+        // repeated ID-value pairs:
+        //     uint64:           size (excluding this field)
+        //     uint32:           ID
+        //     (size - 4) bytes: value
+        // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
+        // uint64:  size (same as the one above)
+        // uint128: magic
+
+        int blocksSize = 0;
+        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
+            blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value
+        }
+
+        int resultSize =
+                8 // size
+                + blocksSize
+                + 8 // size
+                + 16 // magic
+                ;
+        ByteBuffer paddingPair = null;
+        if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
+            int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
+                    (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
+            if (padding < 12) {  // minimum size of an ID-value pair
+                padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
+            }
+            paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
+            paddingPair.putLong(padding - 8);
+            paddingPair.putInt(VERITY_PADDING_BLOCK_ID);
+            paddingPair.rewind();
+            resultSize += padding;
+        }
+
+        ByteBuffer result = ByteBuffer.allocate(resultSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        long blockSizeFieldValue = resultSize - 8L;
+        result.putLong(blockSizeFieldValue);
+
+        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
+            byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
+            int apkSignatureSchemeId = schemeBlockPair.getSecond();
+            long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length;
+            result.putLong(pairSizeFieldValue);
+            result.putInt(apkSignatureSchemeId);
+            result.put(apkSignatureSchemeBlock);
+        }
+
+        if (paddingPair != null) {
+            result.put(paddingPair);
+        }
+
+        result.putLong(blockSizeFieldValue);
+        result.put(APK_SIGNING_BLOCK_MAGIC);
+
+        return result.array();
+    }
+
+    /**
+     * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a
+     * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the
+     * contents / value of the signature block and the second element is the ID of the block.
+     *
+     * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock}
+     */
+    public static List<Pair<byte[], Integer>> getApkSignatureBlocks(
+            DataSource apkSigningBlock) throws IOException {
+        // FORMAT:
+        // uint64:  size (excluding this field)
+        // repeated ID-value pairs:
+        //     uint64:           size (excluding this field)
+        //     uint32:           ID
+        //     (size - 4) bytes: value
+        // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
+        // uint64:  size (same as the one above)
+        // uint128: magic
+        long apkSigningBlockSize = apkSigningBlock.size();
+        if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) {
+            throw new IllegalArgumentException(
+                    "APK signing block size out of range: " + apkSigningBlockSize);
+        }
+        // Remove the header and footer from the signing block to iterate over only the repeated
+        // ID-value pairs.
+        ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8,
+                (int) apkSigningBlock.size() - 32);
+        apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>();
+        while (apkSigningBlockBuffer.hasRemaining()) {
+            long blockLength = apkSigningBlockBuffer.getLong();
+            if (blockLength > Integer.MAX_VALUE || blockLength < 4) {
+                throw new IllegalArgumentException(
+                        "Block index " + (signatureBlocks.size() + 1) + " size out of range: "
+                                + blockLength);
+            }
+            int blockId = apkSigningBlockBuffer.getInt();
+            // Since the block ID has already been read from the signature block read the next
+            // blockLength - 4 bytes as the value.
+            byte[] blockValue = new byte[(int) blockLength - 4];
+            apkSigningBlockBuffer.get(blockValue);
+            signatureBlocks.add(Pair.of(blockValue, blockId));
+        }
+        return signatureBlocks;
+    }
+
+    /**
+     * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code
+     * List} of {@code Pair} instances where the first element is a {@code List} of {@link
+     * X509Certificate}s and the second element is a byte array of the individual signer's block.
+     *
+     * <p>This method supports any signature block that adheres to the following format up to the
+     * signing certificate(s):
+     * <pre>
+     * * length-prefixed sequence of length-prefixed signers
+     *   * length-prefixed signed data
+     *     * length-prefixed sequence of length-prefixed digests:
+     *       * uint32: signature algorithm ID
+     *       * length-prefixed bytes: digest of contents
+     *     * length-prefixed sequence of certificates:
+     *       * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+     * </pre>
+     *
+     * <p>Note, this is a convenience method to obtain any signers from an existing signature block;
+     * the signature of each signer will not be verified.
+     *
+     * @throws ApkFormatException if an error is encountered while parsing the provided {@code
+     * signatureBlock}
+     * @throws CertificateException if the signing certificate(s) within an individual signer block
+     * cannot be parsed
+     */
+    public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners(
+            byte[] signatureBlock) throws ApkFormatException, CertificateException {
+        ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock);
+        signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer);
+        List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>();
+        while (signersBuffer.hasRemaining()) {
+            // Parse the next signer block, save all of its bytes for the resulting List, and
+            // rewind the buffer to allow the signing certificate(s) to be parsed.
+            ByteBuffer signer = getLengthPrefixedSlice(signersBuffer);
+            byte[] signerBytes = new byte[signer.remaining()];
+            signer.get(signerBytes);
+            signer.rewind();
+
+            ByteBuffer signedData = getLengthPrefixedSlice(signer);
+            // The first length prefixed slice is the sequence of digests which are not required
+            // when obtaining the signing certificate(s).
+            getLengthPrefixedSlice(signedData);
+            ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData);
+            List<X509Certificate> certificates = new ArrayList<>();
+            while (certificatesBuffer.hasRemaining()) {
+                int certLength = certificatesBuffer.getInt();
+                byte[] certBytes = new byte[certLength];
+                if (certLength > certificatesBuffer.remaining()) {
+                    throw new IllegalArgumentException(
+                            "Cert index " + (certificates.size() + 1) + " under signer index "
+                                    + (signers.size() + 1) + " size out of range: " + certLength);
+                }
+                certificatesBuffer.get(certBytes);
+                GuaranteedEncodedFormX509Certificate signerCert =
+                        new GuaranteedEncodedFormX509Certificate(
+                                X509CertificateUtils.generateCertificate(certBytes), certBytes);
+                certificates.add(signerCert);
+            }
+            signers.add(Pair.of(certificates, signerBytes));
+        }
+        return signers;
+    }
+
+    /**
+     * Computes the digests of the given APK components according to the algorithms specified in the
+     * given SignerConfigs.
+     *
+     * @param signerConfigs signer configurations, one for each signer At least one signer config
+     *        must be provided.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+     *         missing
+     * @throws SignatureException if an error occurs when computing digests of generating
+     *         signatures
+     */
+    public static Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>>
+            computeContentDigests(
+                    RunnablesExecutor executor,
+                    DataSource beforeCentralDir,
+                    DataSource centralDir,
+                    DataSource eocd,
+                    List<SignerConfig> signerConfigs)
+                            throws IOException, NoSuchAlgorithmException, SignatureException {
+        if (signerConfigs.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "No signer configs provided. At least one is required");
+        }
+
+        // Figure out which digest(s) to use for APK contents.
+        Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1);
+        for (SignerConfig signerConfig : signerConfigs) {
+            for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+                contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
+            }
+        }
+
+        // Compute digests of APK contents.
+        Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
+        try {
+            contentDigests =
+                    computeContentDigests(
+                            executor,
+                            contentDigestAlgorithms,
+                            beforeCentralDir,
+                            centralDir,
+                            eocd);
+        } catch (IOException e) {
+            throw new IOException("Failed to read APK being signed", e);
+        } catch (DigestException e) {
+            throw new SignatureException("Failed to compute digests of APK", e);
+        }
+
+        // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
+        return Pair.of(signerConfigs, contentDigests);
+    }
+
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion)
+            throws NoSupportedSignaturesException {
+        return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+    }
+
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+     * signature within the signing block using the standard JCA.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion,
+            boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException {
+        try {
+            return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion,
+                    maxSdkVersion, onlyRequireJcaSupport);
+        } catch (NoApkSupportedSignaturesException e) {
+            throw new NoSupportedSignaturesException(e.getMessage());
+        }
+    }
+
+    public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException {
+        public NoSupportedSignaturesException(String message) {
+            super(message);
+        }
+    }
+
+    public static class SignatureNotFoundException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public SignatureNotFoundException(String message) {
+            super(message);
+        }
+
+        public SignatureNotFoundException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * uses the SignatureAlgorithms in the provided signerConfig to sign the provided data
+     *
+     * @return list of signature algorithm IDs and their corresponding signatures over the data.
+     */
+    public static List<Pair<Integer, byte[]>> generateSignaturesOverData(
+            SignerConfig signerConfig, byte[] data)
+                    throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+        List<Pair<Integer, byte[]>> signatures =
+                new ArrayList<>(signerConfig.signatureAlgorithms.size());
+        PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+            Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
+            String jcaSignatureAlgorithm = sigAlgAndParams.getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond();
+            byte[] signatureBytes;
+            try {
+                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+                signature.initSign(signerConfig.privateKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    signature.setParameter(jcaSignatureAlgorithmParams);
+                }
+                signature.update(data);
+                signatureBytes = signature.sign();
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
+            } catch (InvalidAlgorithmParameterException | SignatureException e) {
+                throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
+            }
+
+            try {
+                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+                signature.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    signature.setParameter(jcaSignatureAlgorithmParams);
+                }
+                signature.update(data);
+                if (!signature.verify(signatureBytes)) {
+                    throw new SignatureException("Failed to verify generated "
+                            + jcaSignatureAlgorithm
+                            + " signature using public key from certificate");
+                }
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeyException(
+                        "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+                                + " public key from certificate", e);
+            } catch (InvalidAlgorithmParameterException | SignatureException e) {
+                throw new SignatureException(
+                        "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+                                + " public key from certificate", e);
+            }
+
+            signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes));
+        }
+        return signatures;
+    }
+
+    /**
+     * Wrap the signature according to CMS PKCS #7 RFC 5652.
+     * The high-level simplified structure is as follows:
+     * // ContentInfo
+     *     //   digestAlgorithm
+     *     //   SignedData
+     *     //     bag of certificates
+     *     //     SignerInfo
+     *     //       signing cert issuer and serial number (for locating the cert in the above bag)
+     *     //       digestAlgorithm
+     *     //       signatureAlgorithm
+     *     //       signature
+     *
+     * @throws Asn1EncodingException if the ASN.1 structure could not be encoded
+     */
+    public static byte[] generatePkcs7DerEncodedMessage(
+            byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts,
+            AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId)
+            throws Asn1EncodingException, CertificateEncodingException {
+        SignerInfo signerInfo = new SignerInfo();
+        signerInfo.version = 1;
+        X509Certificate signingCert = signerCerts.get(0);
+        X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
+        signerInfo.sid =
+                new SignerIdentifier(
+                        new IssuerAndSerialNumber(
+                                new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
+                                signingCert.getSerialNumber()));
+
+        signerInfo.digestAlgorithm = digestAlgorithmId;
+        signerInfo.signatureAlgorithm = signatureAlgorithmId;
+        signerInfo.signature = ByteBuffer.wrap(signatureBytes);
+
+        SignedData signedData = new SignedData();
+        signedData.certificates = new ArrayList<>(signerCerts.size());
+        for (X509Certificate cert : signerCerts) {
+            signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
+        }
+        signedData.version = 1;
+        signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
+        signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
+        // If data is not null, data will be embedded as is in the result -- an attached pcsk7
+        signedData.encapContentInfo.content = data;
+        signedData.signerInfos = Collections.singletonList(signerInfo);
+        ContentInfo contentInfo = new ContentInfo();
+        contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
+        contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
+        return Asn1DerEncoder.encode(contentInfo);
+    }
+
+    /**
+     * Picks the correct v2/v3 digest for v4 signature verification.
+     *
+     * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils.
+     */
+    public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) {
+        for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) {
+            if (contentDigests.containsKey(algo)) {
+                return contentDigests.get(algo);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Signer configuration.
+     */
+    public static class SignerConfig {
+        /** Private key. */
+        public PrivateKey privateKey;
+
+        /**
+         * Certificates, with the first certificate containing the public key corresponding to
+         * {@link #privateKey}.
+         */
+        public List<X509Certificate> certificates;
+
+        /**
+         * List of signature algorithms with which to sign.
+         */
+        public List<SignatureAlgorithm> signatureAlgorithms;
+
+        public int minSdkVersion;
+        public int maxSdkVersion;
+        public boolean signerTargetsDevRelease;
+        public SigningCertificateLineage signingCertificateLineage;
+    }
+
+    public static class Result extends ApkSigResult {
+        public SigningCertificateLineage signingCertificateLineage = null;
+        public final List<Result.SignerInfo> signers = new ArrayList<>();
+        private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
+        private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
+
+        public Result(int signatureSchemeVersion) {
+            super(signatureSchemeVersion);
+        }
+
+        public boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            if (!signers.isEmpty()) {
+                for (Result.SignerInfo signer : signers) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        public boolean containsWarnings() {
+            if (!mWarnings.isEmpty()) {
+                return true;
+            }
+            if (!signers.isEmpty()) {
+                for (Result.SignerInfo signer : signers) {
+                    if (signer.containsWarnings()) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        public void addError(ApkVerifier.Issue msg, Object... parameters) {
+            mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
+        }
+
+        public void addWarning(ApkVerifier.Issue msg, Object... parameters) {
+            mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
+        }
+
+        @Override
+        public List<ApkVerifier.IssueWithParams> getErrors() {
+            return mErrors;
+        }
+
+        @Override
+        public List<ApkVerifier.IssueWithParams> getWarnings() {
+            return mWarnings;
+        }
+
+        public static class SignerInfo extends ApkSignerInfo {
+            public List<ContentDigest> contentDigests = new ArrayList<>();
+            public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
+            public List<Signature> signatures = new ArrayList<>();
+            public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
+            public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
+            public byte[] signedData;
+            public int minSdkVersion;
+            public int maxSdkVersion;
+            public SigningCertificateLineage signingCertificateLineage;
+
+            private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
+            private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
+
+            public void addError(ApkVerifier.Issue msg, Object... parameters) {
+                mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
+            }
+
+            public void addWarning(ApkVerifier.Issue msg, Object... parameters) {
+                mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public boolean containsWarnings() {
+                return !mWarnings.isEmpty();
+            }
+
+            public List<ApkVerifier.IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<ApkVerifier.IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            public static class ContentDigest {
+                private final int mSignatureAlgorithmId;
+                private final byte[] mValue;
+
+                public ContentDigest(int signatureAlgorithmId, byte[] value) {
+                    mSignatureAlgorithmId  = signatureAlgorithmId;
+                    mValue = value;
+                }
+
+                public int getSignatureAlgorithmId() {
+                    return mSignatureAlgorithmId;
+                }
+
+                public byte[] getValue() {
+                    return mValue;
+                }
+            }
+
+            public static class Signature {
+                private final int mAlgorithmId;
+                private final byte[] mValue;
+
+                public Signature(int algorithmId, byte[] value) {
+                    mAlgorithmId  = algorithmId;
+                    mValue = value;
+                }
+
+                public int getAlgorithmId() {
+                    return mAlgorithmId;
+                }
+
+                public byte[] getValue() {
+                    return mValue;
+                }
+            }
+
+            public static class AdditionalAttribute {
+                private final int mId;
+                private final byte[] mValue;
+
+                public AdditionalAttribute(int id, byte[] value) {
+                    mId  = id;
+                    mValue = value.clone();
+                }
+
+                public int getId() {
+                    return mId;
+                }
+
+                public byte[] getValue() {
+                    return mValue.clone();
+                }
+            }
+        }
+    }
+
+    public static class SupportedSignature extends ApkSupportedSignature {
+        public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+            super(algorithm, signature);
+        }
+    }
+
+    public static class SigningSchemeBlockAndDigests {
+        public final Pair<byte[], Integer> signingSchemeBlock;
+        public final Map<ContentDigestAlgorithm, byte[]> digestInfo;
+
+        public SigningSchemeBlockAndDigests(
+                Pair<byte[], Integer> signingSchemeBlock,
+                Map<ContentDigestAlgorithm, byte[]> digestInfo) {
+            this.signingSchemeBlock = signingSchemeBlock;
+            this.digestInfo = digestInfo;
+        }
+    }
+}

+ 393 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java

@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2020 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.apk;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
+ * utility functionality.
+ */
+public class ApkSigningBlockUtilsLite {
+    private ApkSigningBlockUtilsLite() {}
+
+    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+    /**
+     * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
+     * and the additional information relevant for verifying the block against the file.
+     *
+     * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+     *                identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+     *                block ID.
+     *
+     * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
+     * @throws IOException if an I/O error occurs while reading the APK
+     */
+    public static SignatureInfo findSignature(
+            DataSource apk, ZipSections zipSections, int blockId)
+            throws IOException, SignatureNotFoundException {
+        // Find the APK Signing Block.
+        DataSource apkSigningBlock;
+        long apkSigningBlockOffset;
+        try {
+            ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
+                    ApkUtilsLite.findApkSigningBlock(apk, zipSections);
+            apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+            apkSigningBlock = apkSigningBlockInfo.getContents();
+        } catch (ApkSigningBlockNotFoundException e) {
+            throw new SignatureNotFoundException(e.getMessage(), e);
+        }
+        ByteBuffer apkSigningBlockBuf =
+                apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+        apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
+
+        // Find the APK Signature Scheme Block inside the APK Signing Block.
+        ByteBuffer apkSignatureSchemeBlock =
+                findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
+        return new SignatureInfo(
+                apkSignatureSchemeBlock,
+                apkSigningBlockOffset,
+                zipSections.getZipCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectory());
+    }
+
+    public static ByteBuffer findApkSignatureSchemeBlock(
+            ByteBuffer apkSigningBlock,
+            int blockId) throws SignatureNotFoundException {
+        checkByteOrderLittleEndian(apkSigningBlock);
+        // FORMAT:
+        // OFFSET       DATA TYPE  DESCRIPTION
+        // * @+0  bytes uint64:    size in bytes (excluding this field)
+        // * @+8  bytes pairs
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+        int entryCount = 0;
+        while (pairs.hasRemaining()) {
+            entryCount++;
+            if (pairs.remaining() < 8) {
+                throw new SignatureNotFoundException(
+                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+            }
+            long lenLong = pairs.getLong();
+            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+                throw new SignatureNotFoundException(
+                        "APK Signing Block entry #" + entryCount
+                                + " size out of range: " + lenLong);
+            }
+            int len = (int) lenLong;
+            int nextEntryPos = pairs.position() + len;
+            if (len > pairs.remaining()) {
+                throw new SignatureNotFoundException(
+                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
+                                + ", available: " + pairs.remaining());
+            }
+            int id = pairs.getInt();
+            if (id == blockId) {
+                return getByteBuffer(pairs, len - 4);
+            }
+            pairs.position(nextEntryPos);
+        }
+
+        throw new SignatureNotFoundException(
+                "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
+    }
+
+    public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+        }
+    }
+
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoApkSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion)
+            throws NoApkSupportedSignaturesException {
+        return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+    }
+
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+     * signature within the signing block using the standard JCA.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoApkSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion,
+            boolean onlyRequireJcaSupport) throws
+            NoApkSupportedSignaturesException {
+        // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
+        // Android's behavior on those versions.
+        //
+        // Here we assume that, once introduced, a signature algorithm continues to be supported in
+        // all future Android versions. We also assume that the better-than relationship between
+        // algorithms is exactly the same on all Android platform versions (except that older
+        // platforms might support fewer algorithms). If these assumption are no longer true, the
+        // logic here will need to change accordingly.
+        Map<Integer, T>
+                bestSigAlgorithmOnSdkVersion = new HashMap<>();
+        int minProvidedSignaturesVersion = Integer.MAX_VALUE;
+        for (T sig : signatures) {
+            SignatureAlgorithm sigAlgorithm = sig.algorithm;
+            int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
+                    : sigAlgorithm.getMinSdkVersion();
+            if (sigMinSdkVersion > maxSdkVersion) {
+                continue;
+            }
+            if (sigMinSdkVersion < minProvidedSignaturesVersion) {
+                minProvidedSignaturesVersion = sigMinSdkVersion;
+            }
+
+            T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
+            if ((candidate == null)
+                    || (compareSignatureAlgorithm(
+                    sigAlgorithm, candidate.algorithm) > 0)) {
+                bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
+            }
+        }
+
+        // Must have some supported signature algorithms for minSdkVersion.
+        if (minSdkVersion < minProvidedSignaturesVersion) {
+            throw new NoApkSupportedSignaturesException(
+                    "Minimum provided signature version " + minProvidedSignaturesVersion +
+                            " > minSdkVersion " + minSdkVersion);
+        }
+        if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
+            throw new NoApkSupportedSignaturesException("No supported signature");
+        }
+        List<T> signaturesToVerify =
+                new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
+        Collections.sort(
+                signaturesToVerify,
+                (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
+        return signaturesToVerify;
+    }
+
+    /**
+     * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+     * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+     */
+    public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+        ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+        ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+        return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+    }
+
+    /**
+     * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
+     * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
+     */
+    private static int compareContentDigestAlgorithm(
+            ContentDigestAlgorithm alg1,
+            ContentDigestAlgorithm alg2) {
+        switch (alg1) {
+            case CHUNKED_SHA256:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                        return 0;
+                    case CHUNKED_SHA512:
+                    case VERITY_CHUNKED_SHA256:
+                        return -1;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            case CHUNKED_SHA512:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                    case VERITY_CHUNKED_SHA256:
+                        return 1;
+                    case CHUNKED_SHA512:
+                        return 0;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            case VERITY_CHUNKED_SHA256:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                        return 1;
+                    case VERITY_CHUNKED_SHA256:
+                        return 0;
+                    case CHUNKED_SHA512:
+                        return -1;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            default:
+                throw new IllegalArgumentException("Unknown alg1: " + alg1);
+        }
+    }
+
+    /**
+     * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+     * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+     * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+     * buffer's byte order.
+     */
+    private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+        if (start < 0) {
+            throw new IllegalArgumentException("start: " + start);
+        }
+        if (end < start) {
+            throw new IllegalArgumentException("end < start: " + end + " < " + start);
+        }
+        int capacity = source.capacity();
+        if (end > source.capacity()) {
+            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+        }
+        int originalLimit = source.limit();
+        int originalPosition = source.position();
+        try {
+            source.position(0);
+            source.limit(end);
+            source.position(start);
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            return result;
+        } finally {
+            source.position(0);
+            source.limit(originalLimit);
+            source.position(originalPosition);
+        }
+    }
+
+    /**
+     * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+     * position of this buffer.
+     *
+     * <p>This method reads the next {@code size} bytes at this buffer's current position,
+     * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+     * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+     * {@code size}.
+     */
+    private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
+        if (size < 0) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        int originalLimit = source.limit();
+        int position = source.position();
+        int limit = position + size;
+        if ((limit < position) || (limit > originalLimit)) {
+            throw new BufferUnderflowException();
+        }
+        source.limit(limit);
+        try {
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            source.position(limit);
+            return result;
+        } finally {
+            source.limit(originalLimit);
+        }
+    }
+
+    public static String toHex(byte[] value) {
+        StringBuilder sb = new StringBuilder(value.length * 2);
+        int len = value.length;
+        for (int i = 0; i < len; i++) {
+            int hi = (value[i] & 0xff) >>> 4;
+            int lo = value[i] & 0x0f;
+            sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
+        }
+        return sb.toString();
+    }
+
+    public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+        if (source.remaining() < 4) {
+            throw new ApkFormatException(
+                    "Remaining buffer too short to contain length of length-prefixed field"
+                            + ". Remaining: " + source.remaining());
+        }
+        int len = source.getInt();
+        if (len < 0) {
+            throw new IllegalArgumentException("Negative length");
+        } else if (len > source.remaining()) {
+            throw new ApkFormatException(
+                    "Length-prefixed field longer than remaining buffer"
+                            + ". Field length: " + len + ", remaining: " + source.remaining());
+        }
+        return getByteBuffer(source, len);
+    }
+
+    public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+        int len = buf.getInt();
+        if (len < 0) {
+            throw new ApkFormatException("Negative length");
+        } else if (len > buf.remaining()) {
+            throw new ApkFormatException(
+                    "Underflow while reading length-prefixed value. Length: " + len
+                            + ", available: " + buf.remaining());
+        }
+        byte[] result = new byte[len];
+        buf.get(result);
+        return result;
+    }
+
+    public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+            List<Pair<Integer, byte[]>> sequence) {
+        int resultSize = 0;
+        for (Pair<Integer, byte[]> element : sequence) {
+            resultSize += 12 + element.getSecond().length;
+        }
+        ByteBuffer result = ByteBuffer.allocate(resultSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        for (Pair<Integer, byte[]> element : sequence) {
+            byte[] second = element.getSecond();
+            result.putInt(8 + second.length);
+            result.putInt(element.getFirst());
+            result.putInt(second.length);
+            result.put(second);
+        }
+        return result.array();
+    }
+}

+ 35 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 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.apk;
+
+/**
+ * Base implementation of a supported signature for an APK.
+ */
+public class ApkSupportedSignature {
+    public final SignatureAlgorithm algorithm;
+    public final byte[] signature;
+
+    /**
+     * Constructs a new supported signature using the provided {@code algorithm} and {@code
+     * signature} bytes.
+     */
+    public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+        this.algorithm = algorithm;
+        this.signature = signature;
+    }
+
+}

+ 61 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 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.apk;
+
+/** APK Signature Scheme v2 content digest algorithm. */
+public enum ContentDigestAlgorithm {
+    /** SHA2-256 over 1 MB chunks. */
+    CHUNKED_SHA256(1, "SHA-256", 256 / 8),
+
+    /** SHA2-512 over 1 MB chunks. */
+    CHUNKED_SHA512(2, "SHA-512", 512 / 8),
+
+    /** SHA2-256 over 4 KB chunks for APK verity. */
+    VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
+
+    /** Non-chunk SHA2-256. */
+    SHA256(4, "SHA-256", 256 / 8);
+
+    private final int mId;
+    private final String mJcaMessageDigestAlgorithm;
+    private final int mChunkDigestOutputSizeBytes;
+
+    private ContentDigestAlgorithm(
+            int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+        mId = id;
+        mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
+        mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
+    }
+
+    /** Returns the ID of the digest algorithm used on the APK. */
+    public int getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
+     * chunks by this content digest algorithm.
+     */
+    String getJcaMessageDigestAlgorithm() {
+        return mJcaMessageDigestAlgorithm;
+    }
+
+    /** Returns the size (in bytes) of the digest of a chunk of content. */
+    int getChunkDigestOutputSizeBytes() {
+        return mChunkDigestOutputSizeBytes;
+    }
+}

+ 27 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 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.apk;
+
+/**
+ * Base exception that is thrown when there are no signatures that support the full range of
+ * requested platform versions.
+ */
+public class NoApkSupportedSignaturesException extends Exception {
+    public NoApkSupportedSignaturesException(String message) {
+        super(message);
+    }
+}

+ 225 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java

@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2016 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.apk;
+
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+
+/**
+ * APK Signing Block signature algorithm.
+ */
+public enum SignatureAlgorithm {
+    // TODO reserve the 0x0000 ID to mean null
+    /**
+     * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
+     * digested using SHA2-256 in 1 MB chunks.
+     */
+    RSA_PSS_WITH_SHA256(
+            0x0101,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "RSA",
+            Pair.of("SHA256withRSA/PSS",
+                    new PSSParameterSpec(
+                            "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.M),
+
+    /**
+     * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
+     * digested using SHA2-512 in 1 MB chunks.
+     */
+    RSA_PSS_WITH_SHA512(
+            0x0102,
+            ContentDigestAlgorithm.CHUNKED_SHA512,
+            "RSA",
+            Pair.of(
+                    "SHA512withRSA/PSS",
+                    new PSSParameterSpec(
+                            "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.M),
+
+    /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+    RSA_PKCS1_V1_5_WITH_SHA256(
+            0x0103,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "RSA",
+            Pair.of("SHA256withRSA", null),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
+
+    /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+    RSA_PKCS1_V1_5_WITH_SHA512(
+            0x0104,
+            ContentDigestAlgorithm.CHUNKED_SHA512,
+            "RSA",
+            Pair.of("SHA512withRSA", null),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
+
+    /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+    ECDSA_WITH_SHA256(
+            0x0201,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "EC",
+            Pair.of("SHA256withECDSA", null),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.HONEYCOMB),
+
+    /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+    ECDSA_WITH_SHA512(
+            0x0202,
+            ContentDigestAlgorithm.CHUNKED_SHA512,
+            "EC",
+            Pair.of("SHA512withECDSA", null),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.HONEYCOMB),
+
+    /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+    DSA_WITH_SHA256(
+            0x0301,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "DSA",
+            Pair.of("SHA256withDSA", null),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
+
+    /**
+     * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
+     * deterministically according to RFC 6979.
+     */
+    DETDSA_WITH_SHA256(
+            0x0301,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "DSA",
+            Pair.of("SHA256withDetDSA", null),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
+
+    /**
+     * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
+     * the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
+     * in little endian) construct the final digest.
+     */
+    VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
+            0x0421,
+            ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+            "RSA",
+            Pair.of("SHA256withRSA", null),
+            AndroidSdkVersion.P,
+            AndroidSdkVersion.INITIAL_RELEASE),
+
+    /**
+     * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
+     * fsverity operates. This digest and the content length (before digestion, 8 bytes in little
+     * endian) construct the final digest.
+     */
+    VERITY_ECDSA_WITH_SHA256(
+            0x0423,
+            ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+            "EC",
+            Pair.of("SHA256withECDSA", null),
+            AndroidSdkVersion.P,
+            AndroidSdkVersion.HONEYCOMB),
+
+    /**
+     * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
+     * fsverity operates. This digest and the content length (before digestion, 8 bytes in little
+     * endian) construct the final digest.
+     */
+    VERITY_DSA_WITH_SHA256(
+            0x0425,
+            ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+            "DSA",
+            Pair.of("SHA256withDSA", null),
+            AndroidSdkVersion.P,
+            AndroidSdkVersion.INITIAL_RELEASE);
+
+    private final int mId;
+    private final String mJcaKeyAlgorithm;
+    private final ContentDigestAlgorithm mContentDigestAlgorithm;
+    private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
+    private final int mMinSdkVersion;
+    private final int mJcaSigAlgMinSdkVersion;
+
+    SignatureAlgorithm(int id,
+            ContentDigestAlgorithm contentDigestAlgorithm,
+            String jcaKeyAlgorithm,
+            Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
+            int minSdkVersion,
+            int jcaSigAlgMinSdkVersion) {
+        mId = id;
+        mContentDigestAlgorithm = contentDigestAlgorithm;
+        mJcaKeyAlgorithm = jcaKeyAlgorithm;
+        mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
+        mMinSdkVersion = minSdkVersion;
+        mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
+    }
+
+    /**
+     * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
+     */
+    public int getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the content digest algorithm associated with this signature algorithm.
+     */
+    public ContentDigestAlgorithm getContentDigestAlgorithm() {
+        return mContentDigestAlgorithm;
+    }
+
+    /**
+     * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
+     */
+    public String getJcaKeyAlgorithm() {
+        return mJcaKeyAlgorithm;
+    }
+
+    /**
+     * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
+     * (or null if not needed) to parameterize the {@code Signature}.
+     */
+    public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
+        return mJcaSignatureAlgAndParams;
+    }
+
+    public int getMinSdkVersion() {
+        return mMinSdkVersion;
+    }
+
+    /**
+     * Returns the minimum SDK version that supports the JCA signature algorithm.
+     */
+    public int getJcaSigAlgMinSdkVersion() {
+        return mJcaSigAlgMinSdkVersion;
+    }
+
+    public static SignatureAlgorithm findById(int id) {
+        for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+            if (alg.getId() == id) {
+                return alg;
+            }
+        }
+
+        return null;
+    }
+}

+ 53 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java

@@ -0,0 +1,53 @@
+/*
+ * 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.apk;
+
+import java.nio.ByteBuffer;
+
+/**
+ * APK Signature Scheme block and additional information relevant to verifying the signatures
+ * contained in the block against the file.
+ */
+public class SignatureInfo {
+    /** Contents of APK Signature Scheme block. */
+    public final ByteBuffer signatureBlock;
+
+    /** Position of the APK Signing Block in the file. */
+    public final long apkSigningBlockOffset;
+
+    /** Position of the ZIP Central Directory in the file. */
+    public final long centralDirOffset;
+
+    /** Position of the ZIP End of Central Directory (EoCD) in the file. */
+    public final long eocdOffset;
+
+    /** Contents of ZIP End of Central Directory (EoCD) of the file. */
+    public final ByteBuffer eocd;
+
+    public SignatureInfo(
+            ByteBuffer signatureBlock,
+            long apkSigningBlockOffset,
+            long centralDirOffset,
+            long eocdOffset,
+            ByteBuffer eocd) {
+        this.signatureBlock = signatureBlock;
+        this.apkSigningBlockOffset = apkSigningBlockOffset;
+        this.centralDirOffset = centralDirOffset;
+        this.eocdOffset = eocdOffset;
+        this.eocd = eocd;
+    }
+}

+ 30 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 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.apk;
+
+/**
+ * Base exception that is thrown when the APK is not signed with the requested signature scheme.
+ */
+public class SignatureNotFoundException extends Exception {
+    public SignatureNotFoundException(String message) {
+        super(message);
+    }
+
+    public SignatureNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 235 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java

@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+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.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
+public class SourceStampCertificateLineage {
+
+    private final static int FIRST_VERSION = 1;
+    private final static int CURRENT_VERSION = FIRST_VERSION;
+
+    /**
+     * Deserializes the binary representation of a SourceStampCertificateLineage. Also
+     * verifies that the structure is well-formed, e.g. that the signature for each node is from its
+     * parent.
+     */
+    public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
+            throws IOException {
+        List<SigningCertificateNode> result = new ArrayList<>();
+        int nodeCount = 0;
+        if (inputBytes == null || !inputBytes.hasRemaining()) {
+            return null;
+        }
+
+        ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+        }
+
+        // FORMAT (little endian):
+        // * uint32: version code
+        // * sequence of length-prefixed (uint32): nodes
+        //   * length-prefixed bytes: signed data
+        //     * length-prefixed bytes: certificate
+        //     * uint32: signature algorithm id
+        //   * uint32: flags
+        //   * uint32: signature algorithm id (used by to sign next cert in lineage)
+        //   * length-prefixed bytes: signature over above signed data
+
+        X509Certificate lastCert = null;
+        int lastSigAlgorithmId = 0;
+
+        try {
+            int version = inputBytes.getInt();
+            if (version != CURRENT_VERSION) {
+                // we only have one version to worry about right now, so just check it
+                throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+                        + " different than any of which we are aware");
+            }
+            HashSet<X509Certificate> certHistorySet = new HashSet<>();
+            while (inputBytes.hasRemaining()) {
+                nodeCount++;
+                ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
+                ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
+                int flags = nodeBytes.getInt();
+                int sigAlgorithmId = nodeBytes.getInt();
+                SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
+                byte[] signature = readLengthPrefixedByteArray(nodeBytes);
+
+                if (lastCert != null) {
+                    // Use previous level cert to verify current level
+                    String jcaSignatureAlgorithm =
+                            sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+                    AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                            sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+                    PublicKey publicKey = lastCert.getPublicKey();
+                    Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                    sig.initVerify(publicKey);
+                    if (jcaSignatureAlgorithmParams != null) {
+                        sig.setParameter(jcaSignatureAlgorithmParams);
+                    }
+                    sig.update(signedData);
+                    if (!sig.verify(signature)) {
+                        throw new SecurityException("Unable to verify signature of certificate #"
+                                + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+                                + " SourceStampCertificateLineage object");
+                    }
+                }
+
+                signedData.rewind();
+                byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+                int signedSigAlgorithm = signedData.getInt();
+                if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
+                    throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+                            + nodeBytes + " when verifying SourceStampCertificateLineage object");
+                }
+                lastCert = (X509Certificate) certFactory.generateCertificate(
+                    new ByteArrayInputStream(encodedCert));
+                lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
+                if (certHistorySet.contains(lastCert)) {
+                    throw new SecurityException("Encountered duplicate entries in "
+                            + "SigningCertificateLineage at certificate #" + nodeCount + ".  All "
+                            + "signing certificates should be unique");
+                }
+                certHistorySet.add(lastCert);
+                lastSigAlgorithmId = sigAlgorithmId;
+                result.add(new SigningCertificateNode(
+                        lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
+                        SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
+            }
+        } catch(ApkFormatException | BufferUnderflowException e){
+            throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
+        } catch(NoSuchAlgorithmException | InvalidKeyException
+                | InvalidAlgorithmParameterException | SignatureException e){
+            throw new SecurityException(
+                    "Failed to verify signature over signed data for certificate #" + nodeCount
+                            + " when parsing SourceStampCertificateLineage object", e);
+        } catch(CertificateException e){
+            throw new SecurityException("Failed to decode certificate #" + nodeCount
+                    + " when parsing SourceStampCertificateLineage object", e);
+        }
+        return result;
+    }
+
+    /**
+     * Represents one signing certificate in the SourceStampCertificateLineage, which
+     * generally means it is/was used at some point to sign source stamps.
+     */
+    public static class SigningCertificateNode {
+
+        public SigningCertificateNode(
+                X509Certificate signingCert,
+                SignatureAlgorithm parentSigAlgorithm,
+                SignatureAlgorithm sigAlgorithm,
+                byte[] signature,
+                int flags) {
+            this.signingCert = signingCert;
+            this.parentSigAlgorithm = parentSigAlgorithm;
+            this.sigAlgorithm = sigAlgorithm;
+            this.signature = signature;
+            this.flags = flags;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof SigningCertificateNode)) return false;
+
+            SigningCertificateNode that = (SigningCertificateNode) o;
+            if (!signingCert.equals(that.signingCert)) return false;
+            if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
+            if (sigAlgorithm != that.sigAlgorithm) return false;
+            if (!Arrays.equals(signature, that.signature)) return false;
+            if (flags != that.flags) return false;
+
+            // we made it
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
+            result = prime * result +
+                ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
+            result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
+            result = prime * result + Arrays.hashCode(signature);
+            result = prime * result + flags;
+            return result;
+        }
+
+        /**
+         * the signing cert for this node.  This is part of the data signed by the parent node.
+         */
+        public final X509Certificate signingCert;
+
+        /**
+         * the algorithm used by this node's parent to bless this data.  Its ID value is part of
+         * the data signed by the parent node. {@code null} for first node.
+         */
+        public final SignatureAlgorithm parentSigAlgorithm;
+
+        /**
+         * the algorithm used by this node to bless the next node's data.  Its ID value is part
+         * of the signed data of the next node. {@code null} for the last node.
+         */
+        public SignatureAlgorithm sigAlgorithm;
+
+        /**
+         * signature over the signed data (above).  The signature is from this node's parent
+         * signing certificate, which should correspond to the signing certificate used to sign an
+         * APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
+         */
+        public final byte[] signature;
+
+        /**
+         * the flags detailing how the platform should treat this signing cert
+         */
+        public int flags;
+    }
+}

+ 34 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+/** Constants used for source stamp signing and verification. */
+public class SourceStampConstants {
+    private SourceStampConstants() {}
+
+    public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+    public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+    public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
+    /**
+     * The source stamp timestamp attribute value is an 8-byte little-endian encoded long
+     * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
+     * of the attribute value buffer will be used to read the timestamp, and any additional buffer
+     * space will be ignored.
+     */
+    public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
+}

+ 364 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java

@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
+
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSupportedSignature;
+import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+
+import java.io.ByteArrayInputStream;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+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.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ */
+class SourceStampVerifier {
+    /** Hidden constructor to prevent instantiation. */
+    private SourceStampVerifier() {
+    }
+
+    /**
+     * Parses the SourceStamp block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over digest provided.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+     * maxSdkVersion]} range.
+     */
+    public static void verifyV1SourceStamp(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            ApkSignerInfo result,
+            byte[] apkDigest,
+            byte[] sourceStampCertificateDigest,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws ApkFormatException, NoSuchAlgorithmException {
+        X509Certificate sourceStampCertificate =
+                verifySourceStampCertificate(
+                        sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+        if (result.containsWarnings() || result.containsErrors()) {
+            return;
+        }
+
+        ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+        verifySourceStampSignature(
+                apkDigest,
+                minSdkVersion,
+                maxSdkVersion,
+                sourceStampCertificate,
+                apkDigestSignatures,
+                result);
+    }
+
+    /**
+     * Parses the SourceStamp block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over digest of multiple signature schemes provided.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+     * maxSdkVersion]} range.
+     */
+    public static void verifyV2SourceStamp(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            ApkSignerInfo result,
+            Map<Integer, byte[]> signatureSchemeApkDigests,
+            byte[] sourceStampCertificateDigest,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws ApkFormatException, NoSuchAlgorithmException {
+        X509Certificate sourceStampCertificate =
+                verifySourceStampCertificate(
+                        sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+        if (result.containsWarnings() || result.containsErrors()) {
+            return;
+        }
+
+        // Parse signed signature schemes block.
+        ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
+        Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
+        while (signedSignatureSchemes.hasRemaining()) {
+            ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
+            int signatureSchemeId = signedSignatureScheme.getInt();
+            ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
+            signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
+        }
+
+        for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
+                signatureSchemeApkDigests.entrySet()) {
+            // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
+            // v3.0 block must always be present with a v3.1 block is it sufficient to just use the
+            // v3.0 block?
+            if (signatureSchemeApkDigest.getKey()
+                    == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
+                continue;
+            }
+            if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
+                result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
+                return;
+            }
+            verifySourceStampSignature(
+                    signatureSchemeApkDigest.getValue(),
+                    minSdkVersion,
+                    maxSdkVersion,
+                    sourceStampCertificate,
+                    signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
+                    result);
+            if (result.containsWarnings() || result.containsErrors()) {
+                return;
+            }
+        }
+
+        if (sourceStampBlockData.hasRemaining()) {
+            // The stamp block contains some additional attributes.
+            ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
+            ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+
+            byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
+            stampAttributeData.get(stampAttributeBytes);
+            stampAttributeData.flip();
+
+            verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
+                    sourceStampCertificate, stampAttributeDataSignatures, result);
+            if (result.containsErrors() || result.containsWarnings()) {
+                return;
+            }
+            parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
+        }
+    }
+
+    private static X509Certificate verifySourceStampCertificate(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            byte[] sourceStampCertificateDigest,
+            ApkSignerInfo result)
+            throws NoSuchAlgorithmException, ApkFormatException {
+        // Parse the SourceStamp certificate.
+        byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
+        X509Certificate sourceStampCertificate;
+        try {
+            sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
+                    new ByteArrayInputStream(sourceStampEncodedCertificate));
+        } catch (CertificateException e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+            return null;
+        }
+        // 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.
+        sourceStampCertificate =
+                new GuaranteedEncodedFormX509Certificate(
+                        sourceStampCertificate, sourceStampEncodedCertificate);
+        result.certs.add(sourceStampCertificate);
+        // Verify the SourceStamp certificate found in the signing block is the same as the
+        // SourceStamp certificate found in the APK.
+        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+        messageDigest.update(sourceStampEncodedCertificate);
+        byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
+        if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
+            result.addWarning(
+                    ApkVerificationIssue
+                            .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+                    toHex(sourceStampBlockCertificateDigest),
+                    toHex(sourceStampCertificateDigest));
+            return null;
+        }
+        return sourceStampCertificate;
+    }
+
+    private static void verifySourceStampSignature(
+            byte[] data,
+            int minSdkVersion,
+            int maxSdkVersion,
+            X509Certificate sourceStampCertificate,
+            ByteBuffer signatures,
+            ApkSignerInfo result) {
+        // Parse the signatures block and identify supported signatures
+        int signatureCount = 0;
+        List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
+        while (signatures.hasRemaining()) {
+            signatureCount++;
+            try {
+                ByteBuffer signature = getLengthPrefixedSlice(signatures);
+                int sigAlgorithmId = signature.getInt();
+                byte[] sigBytes = readLengthPrefixedByteArray(signature);
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    result.addInfoMessage(
+                            ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+                            sigAlgorithmId);
+                    continue;
+                }
+                supportedSignatures.add(
+                        new ApkSupportedSignature(signatureAlgorithm, sigBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addWarning(
+                        ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+                return;
+            }
+        }
+        if (supportedSignatures.isEmpty()) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
+            return;
+        }
+        // Verify signatures over digests using the SourceStamp's certificate.
+        List<ApkSupportedSignature> signaturesToVerify;
+        try {
+            signaturesToVerify =
+                    getSignaturesToVerify(
+                            supportedSignatures, minSdkVersion, maxSdkVersion, true);
+        } catch (NoApkSupportedSignaturesException e) {
+            // To facilitate debugging capture the signature algorithms and resulting exception in
+            // the warning.
+            StringBuilder signatureAlgorithms = new StringBuilder();
+            for (ApkSupportedSignature supportedSignature : supportedSignatures) {
+                if (signatureAlgorithms.length() > 0) {
+                    signatureAlgorithms.append(", ");
+                }
+                signatureAlgorithms.append(supportedSignature.algorithm);
+            }
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+                    signatureAlgorithms.toString(), e);
+            return;
+        }
+        for (ApkSupportedSignature signature : signaturesToVerify) {
+            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+            String jcaSignatureAlgorithm =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+            PublicKey publicKey = sourceStampCertificate.getPublicKey();
+            try {
+                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                sig.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    sig.setParameter(jcaSignatureAlgorithmParams);
+                }
+                sig.update(data);
+                byte[] sigBytes = signature.signature;
+                if (!sig.verify(sigBytes)) {
+                    result.addWarning(
+                            ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+                    return;
+                }
+            } catch (InvalidKeyException
+                    | InvalidAlgorithmParameterException
+                    | SignatureException
+                    | NoSuchAlgorithmException e) {
+                result.addWarning(
+                        ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                return;
+            }
+        }
+    }
+
+    private static void parseStampAttributes(ByteBuffer stampAttributeData,
+            X509Certificate sourceStampCertificate, ApkSignerInfo result)
+            throws ApkFormatException {
+        ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
+        int stampAttributeCount = 0;
+        while (stampAttributes.hasRemaining()) {
+            stampAttributeCount++;
+            try {
+                ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
+                int id = attribute.getInt();
+                byte[] value = ByteBufferUtils.toByteArray(attribute);
+                if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
+                    readStampCertificateLineage(value, sourceStampCertificate, result);
+                } else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
+                    long timestamp = ByteBuffer.wrap(value).order(
+                            ByteOrder.LITTLE_ENDIAN).getLong();
+                    if (timestamp > 0) {
+                        result.timestamp = timestamp;
+                    } else {
+                        result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+                                timestamp);
+                    }
+                } else {
+                    result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
+                }
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+                        stampAttributeCount);
+                return;
+            }
+        }
+    }
+
+    private static void readStampCertificateLineage(byte[] lineageBytes,
+            X509Certificate sourceStampCertificate, ApkSignerInfo result) {
+        try {
+            // SourceStampCertificateLineage is verified when built
+            List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
+                    SourceStampCertificateLineage.readSigningCertificateLineage(
+                            ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
+            for (int i = 0; i < nodes.size(); i++) {
+                result.certificateLineage.add(nodes.get(i).signingCert);
+            }
+            // Make sure that the last cert in the chain matches this signer cert
+            if (!sourceStampCertificate.equals(
+                    result.certificateLineage.get(result.certificateLineage.size() - 1))) {
+                result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+            }
+        } catch (SecurityException e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+        } catch (IllegalArgumentException e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+        } catch (Exception e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
+        }
+    }
+}

+ 109 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java

@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
+ */
+public abstract class V1SourceStampSigner {
+    public static final int V1_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+
+    /** Hidden constructor to prevent instantiation. */
+    private V1SourceStampSigner() {}
+
+    public static Pair<byte[], Integer> generateSourceStampBlock(
+            SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
+            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        if (sourceStampSignerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+            digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+        }
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+        SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+        try {
+            sourceStampBlock.stampCertificate =
+                    sourceStampSignerConfig.certificates.get(0).getEncoded();
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException(
+                    "Retrieving the encoded form of the stamp certificate failed", e);
+        }
+
+        byte[] digestBytes =
+                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+        sourceStampBlock.signedDigests =
+                ApkSigningBlockUtils.generateSignaturesOverData(
+                        sourceStampSignerConfig, digestBytes);
+
+        // FORMAT:
+        // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+        // * length-prefixed sequence of length-prefixed signatures:
+        //   * uint32: signature algorithm ID
+        //   * length-prefixed bytes: signature of signed data
+        byte[] sourceStampSignerBlock =
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            sourceStampBlock.stampCertificate,
+                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                    sourceStampBlock.signedDigests),
+                        });
+
+        // FORMAT:
+        // * length-prefixed stamp block.
+        return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+                SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
+    }
+
+    private static final class SourceStampBlock {
+        public byte[] stampCertificate;
+        public List<Pair<Integer, byte[]>> signedDigests;
+    }
+}

+ 139 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java

@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
+ */
+public abstract class V1SourceStampVerifier {
+
+    /** Hidden constructor to prevent instantiation. */
+    private V1SourceStampVerifier() {}
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+     * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see {@link
+     * ApkSigningBlockUtils.Result#getErrors()}.
+     *
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *     required cryptographic algorithm implementation is missing
+     * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+     *     found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigningBlockUtils.Result verify(
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            byte[] sourceStampCertificateDigest,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException,
+                    ApkSigningBlockUtils.SignatureNotFoundException {
+        ApkSigningBlockUtils.Result result =
+                new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(
+                        apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
+
+        verify(
+                signatureInfo.signatureBlock,
+                sourceStampCertificateDigest,
+                apkContentDigests,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+     * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+     * more information about the contract of this method.
+     */
+    private static void verify(
+            ByteBuffer sourceStampBlock,
+            byte[] sourceStampCertificateDigest,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigningBlockUtils.Result result)
+            throws NoSuchAlgorithmException {
+        ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+                new ApkSigningBlockUtils.Result.SignerInfo();
+        result.signers.add(signerInfo);
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            ByteBuffer sourceStampBlockData =
+                    ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+            byte[] digestBytes =
+                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                            getApkDigests(apkContentDigests));
+            SourceStampVerifier.verifyV1SourceStamp(
+                    sourceStampBlockData,
+                    certFactory,
+                    signerInfo,
+                    digestBytes,
+                    sourceStampCertificateDigest,
+                    minSdkVersion,
+                    maxSdkVersion);
+            result.verified = !result.containsErrors() && !result.containsWarnings();
+        } catch (CertificateException e) {
+            throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+        } catch (ApkFormatException | BufferUnderflowException e) {
+            signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+        }
+    }
+
+    private static List<Pair<Integer, byte[]>> getApkDigests(
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+                apkContentDigests.entrySet()) {
+            digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+        }
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+        return digests;
+    }
+}

+ 286 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java

@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.SigningCertificateLineage;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
+ */
+public class V2SourceStampSigner {
+    public static final int V2_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+    private final SignerConfig mSourceStampSignerConfig;
+    private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
+    private final boolean mSourceStampTimestampEnabled;
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SourceStampSigner(Builder builder) {
+        mSourceStampSignerConfig = builder.mSourceStampSignerConfig;
+        mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos;
+        mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled;
+    }
+
+    public static Pair<byte[], Integer> generateSourceStampBlock(
+            SignerConfig sourceStampSignerConfig,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
+            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        return new Builder(sourceStampSignerConfig,
+                signatureSchemeDigestInfos).build().generateSourceStampBlock();
+    }
+
+    public Pair<byte[], Integer> generateSourceStampBlock()
+            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        if (mSourceStampSignerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+
+        // Extract the digests for signature schemes.
+        List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
+        getSignedDigestsFor(
+                VERSION_APK_SIGNATURE_SCHEME_V3,
+                mSignatureSchemeDigestInfos,
+                mSourceStampSignerConfig,
+                signatureSchemeDigests);
+        getSignedDigestsFor(
+                VERSION_APK_SIGNATURE_SCHEME_V2,
+                mSignatureSchemeDigestInfos,
+                mSourceStampSignerConfig,
+                signatureSchemeDigests);
+        getSignedDigestsFor(
+                VERSION_JAR_SIGNATURE_SCHEME,
+                mSignatureSchemeDigestInfos,
+                mSourceStampSignerConfig,
+                signatureSchemeDigests);
+        Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
+
+        SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+        try {
+            sourceStampBlock.stampCertificate =
+                    mSourceStampSignerConfig.certificates.get(0).getEncoded();
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException(
+                    "Retrieving the encoded form of the stamp certificate failed", e);
+        }
+
+        sourceStampBlock.signedDigests = signatureSchemeDigests;
+
+        sourceStampBlock.stampAttributes = encodeStampAttributes(
+                generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage));
+        sourceStampBlock.signedStampAttributes =
+                ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
+                        sourceStampBlock.stampAttributes);
+
+        // FORMAT:
+        // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+        // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+        //   * uint32: signature scheme id
+        //   * length-prefixed bytes: signed digests for the respective signature scheme
+        // * length-prefixed bytes: encoded stamp attributes
+        // * length-prefixed sequence of length-prefixed signed stamp attributes:
+        //   * uint32: signature algorithm id
+        //   * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
+        byte[] sourceStampSignerBlock =
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][]{
+                                sourceStampBlock.stampCertificate,
+                                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                        sourceStampBlock.signedDigests),
+                                sourceStampBlock.stampAttributes,
+                                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                        sourceStampBlock.signedStampAttributes),
+                        });
+
+        // FORMAT:
+        // * length-prefixed stamp block.
+        return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+                SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+    }
+
+    private static void getSignedDigestsFor(
+            int signatureSchemeVersion,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
+            SignerConfig mSourceStampSignerConfig,
+            List<Pair<Integer, byte[]>> signatureSchemeDigests)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
+            return;
+        }
+
+        Map<ContentDigestAlgorithm, byte[]> digestInfo =
+                mSignatureSchemeDigestInfos.get(signatureSchemeVersion);
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+            digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+        }
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed digests:
+        //   * uint32: digest algorithm id
+        //   * length-prefixed bytes: digest of the respective digest algorithm
+        byte[] digestBytes =
+                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed signed digests:
+        //   * uint32: signature algorithm id
+        //   * length-prefixed bytes: signed digest for the respective signature algorithm
+        List<Pair<Integer, byte[]>> signedDigest =
+                ApkSigningBlockUtils.generateSignaturesOverData(
+                        mSourceStampSignerConfig, digestBytes);
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+        //   * uint32: signature scheme id
+        //   * length-prefixed bytes: signed digests for the respective signature scheme
+        signatureSchemeDigests.add(
+                Pair.of(
+                        signatureSchemeVersion,
+                        encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                signedDigest)));
+    }
+
+    private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
+        int payloadSize = 0;
+        for (byte[] attributeValue : stampAttributes.values()) {
+            // Pair size + Attribute ID + Attribute value
+            payloadSize += 4 + 4 + attributeValue.length;
+        }
+
+        // FORMAT (little endian):
+        // * length-prefixed bytes: pair
+        //   * uint32: ID
+        //   * bytes: value
+        ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(payloadSize);
+        for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
+            // Pair size
+            result.putInt(4 + stampAttribute.getValue().length);
+            result.putInt(stampAttribute.getKey());
+            result.put(stampAttribute.getValue());
+        }
+        return result.array();
+    }
+
+    private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
+        HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
+
+        if (mSourceStampTimestampEnabled) {
+            // Write the current epoch time as the timestamp for the source stamp.
+            long timestamp = Instant.now().getEpochSecond();
+            if (timestamp > 0) {
+                ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
+                attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
+                attributeBuffer.putLong(timestamp);
+                stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID,
+                        attributeBuffer.array());
+            } else {
+                // The epoch time should never be <= 0, and since security decisions can potentially
+                // be made based on the value in the timestamp, throw an Exception to ensure the
+                // issues with the environment are resolved before allowing the signing.
+                throw new IllegalStateException(
+                        "Received an invalid value from Instant#getTimestamp: " + timestamp);
+            }
+        }
+
+        if (lineage != null) {
+            stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
+                    lineage.encodeSigningCertificateLineage());
+        }
+        return stampAttributes;
+    }
+
+    private static final class SourceStampBlock {
+        public byte[] stampCertificate;
+        public List<Pair<Integer, byte[]>> signedDigests;
+        // Optional stamp attributes that are not required for verification.
+        public byte[] stampAttributes;
+        public List<Pair<Integer, byte[]>> signedStampAttributes;
+    }
+
+    /** Builder of {@link V2SourceStampSigner} instances. */
+    public static class Builder {
+        private final SignerConfig mSourceStampSignerConfig;
+        private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
+        private boolean mSourceStampTimestampEnabled = true;
+
+        /**
+         * Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig}
+         * and the {@code signatureSchemeDigestInfos}.
+         */
+        public Builder(SignerConfig sourceStampSignerConfig,
+                Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) {
+            mSourceStampSignerConfig = sourceStampSignerConfig;
+            mSignatureSchemeDigestInfos = signatureSchemeDigestInfos;
+        }
+
+        /**
+         * Sets whether the source stamp should contain the timestamp attribute with the time
+         * at which the source stamp was signed.
+         */
+        public Builder setSourceStampTimestampEnabled(boolean value) {
+            mSourceStampTimestampEnabled = value;
+            return this;
+        }
+
+        /**
+         * Builds a new V2SourceStampSigner that can be used to generate a new source stamp
+         * block signed with the specified signing config.
+         */
+        public V2SourceStampSigner build() {
+            return new V2SourceStampSigner(this);
+        }
+    }
+}

+ 159 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
+ */
+public abstract class V2SourceStampVerifier {
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SourceStampVerifier() {}
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+     * The APK must be considered verified only if {@link ApkSigResult#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see {@link
+     * ApkSigResult#getErrors()}.
+     *
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *     required cryptographic algorithm implementation is missing
+     * @throws SignatureNotFoundException if no SourceStamp signatures are
+     *     found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigResult verify(
+            DataSource apk,
+            ZipSections zipSections,
+            byte[] sourceStampCertificateDigest,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+        ApkSigResult result =
+                new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtilsLite.findSignature(
+                        apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
+
+        verify(
+                signatureInfo.signatureBlock,
+                sourceStampCertificateDigest,
+                signatureSchemeApkContentDigests,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+     * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
+     * more information about the contract of this method.
+     */
+    private static void verify(
+            ByteBuffer sourceStampBlock,
+            byte[] sourceStampCertificateDigest,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigResult result)
+            throws NoSuchAlgorithmException {
+        ApkSignerInfo signerInfo = new ApkSignerInfo();
+        result.mSigners.add(signerInfo);
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            ByteBuffer sourceStampBlockData =
+                    ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
+            SourceStampVerifier.verifyV2SourceStamp(
+                    sourceStampBlockData,
+                    certFactory,
+                    signerInfo,
+                    getSignatureSchemeDigests(signatureSchemeApkContentDigests),
+                    sourceStampCertificateDigest,
+                    minSdkVersion,
+                    maxSdkVersion);
+            result.verified = !result.containsErrors() && !result.containsWarnings();
+        } catch (CertificateException e) {
+            throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+        } catch (ApkFormatException | BufferUnderflowException e) {
+            signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+        }
+    }
+
+    private static Map<Integer, byte[]> getSignatureSchemeDigests(
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
+        Map<Integer, byte[]> digests = new HashMap<>();
+        for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
+                signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
+            List<Pair<Integer, byte[]>> apkDigests =
+                    getApkDigests(signatureSchemeApkContentDigest.getValue());
+            digests.put(
+                    signatureSchemeApkContentDigest.getKey(),
+                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
+        }
+        return digests;
+    }
+
+    private static List<Pair<Integer, byte[]>> getApkDigests(
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+                apkContentDigests.entrySet()) {
+            digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+        }
+        Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
+            @Override
+            public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
+                return pair1.getFirst() - pair2.getFirst();
+            }
+        });
+        return digests;
+    }
+}

+ 74 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 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.apk.v1;
+
+import java.util.Comparator;
+
+/**
+ * Digest algorithm used with JAR signing (aka v1 signing scheme).
+ */
+public enum DigestAlgorithm {
+    /** SHA-1 */
+    SHA1("SHA-1"),
+
+    /** SHA2-256 */
+    SHA256("SHA-256");
+
+    private final String mJcaMessageDigestAlgorithm;
+
+    private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
+        mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
+    }
+
+    /**
+     * Returns the {@link java.security.MessageDigest} algorithm represented by this digest
+     * algorithm.
+     */
+    String getJcaMessageDigestAlgorithm() {
+        return mJcaMessageDigestAlgorithm;
+    }
+
+    public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
+
+    private static class StrengthComparator implements Comparator<DigestAlgorithm> {
+        @Override
+        public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
+            switch (a1) {
+                case SHA1:
+                    switch (a2) {
+                        case SHA1:
+                            return 0;
+                        case SHA256:
+                            return -1;
+                    }
+                    throw new RuntimeException("Unsupported algorithm: " + a2);
+
+                case SHA256:
+                    switch (a2) {
+                        case SHA1:
+                            return 1;
+                        case SHA256:
+                            return 0;
+                    }
+                    throw new RuntimeException("Unsupported algorithm: " + a2);
+
+                default:
+                    throw new RuntimeException("Unsupported algorithm: " + a1);
+            }
+        }
+    }
+}

+ 26 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 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.apk.v1;
+
+/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
+public class V1SchemeConstants {
+    private V1SchemeConstants() {}
+
+    public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+    public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
+            "X-Android-APK-Signed";
+}

+ 586 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java

@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2016 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.apk.v1;
+
+import static com.android.apksig.Constants.MAX_APK_SIGNERS;
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.jar.ManifestWriter;
+import com.android.apksig.internal.jar.SignatureFileWriter;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+/**
+ * APK signer which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
+ */
+public abstract class V1SchemeSigner {
+    public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+    private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
+            new Attributes.Name("Created-By");
+    private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
+    private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
+
+    private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
+            new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+
+    /**
+     * Signer configuration.
+     */
+    public static class SignerConfig {
+        /** Name. */
+        public String name;
+
+        /** Private key. */
+        public PrivateKey privateKey;
+
+        /**
+         * Certificates, with the first certificate containing the public key corresponding to
+         * {@link #privateKey}.
+         */
+        public List<X509Certificate> certificates;
+
+        /**
+         * Digest algorithm used for the signature.
+         */
+        public DigestAlgorithm signatureDigestAlgorithm;
+
+        /**
+         * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
+         */
+        public boolean deterministicDsaSigning;
+    }
+
+    /** Hidden constructor to prevent instantiation. */
+    private V1SchemeSigner() {}
+
+    /**
+     * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
+     *
+     * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+     *        AndroidManifest.xml minSdkVersion attribute)
+     *
+     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
+     *         JAR signing (aka v1 signature scheme)
+     */
+    public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
+            PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
+        String keyAlgorithm = signingKey.getAlgorithm();
+        if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
+            // Prior to API Level 18, only SHA-1 can be used with RSA.
+            if (minSdkVersion < 18) {
+                return DigestAlgorithm.SHA1;
+            }
+            return DigestAlgorithm.SHA256;
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            // Prior to API Level 21, only SHA-1 can be used with DSA
+            if (minSdkVersion < 21) {
+                return DigestAlgorithm.SHA1;
+            } else {
+                return DigestAlgorithm.SHA256;
+            }
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            if (minSdkVersion < 18) {
+                throw new InvalidKeyException(
+                        "ECDSA signatures only supported for minSdkVersion 18 and higher");
+            }
+            return DigestAlgorithm.SHA256;
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    /**
+     * Returns a safe version of the provided signer name.
+     */
+    public static String getSafeSignerName(String name) {
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("Empty name");
+        }
+
+        // According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
+        // name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
+        StringBuilder result = new StringBuilder();
+        char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
+        for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
+            char c = nameCharsUpperCase[i];
+            if (((c >= 'A') && (c <= 'Z'))
+                    || ((c >= '0') && (c <= '9'))
+                    || (c == '-')
+                    || (c == '_')) {
+                result.append(c);
+            } else {
+                result.append('_');
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
+     */
+    private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
+            throws NoSuchAlgorithmException {
+        String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+        return MessageDigest.getInstance(jcaAlgorithm);
+    }
+
+    /**
+     * Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
+     * algorithm.
+     */
+    public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
+        return digestAlgorithm.getJcaMessageDigestAlgorithm();
+    }
+
+    /**
+     * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
+     * manifest.
+     */
+    public static boolean isJarEntryDigestNeededInManifest(String entryName) {
+        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
+
+        // Entries which represent directories sould not be listed in the manifest.
+        if (entryName.endsWith("/")) {
+            return false;
+        }
+
+        // Entries outside of META-INF must be listed in the manifest.
+        if (!entryName.startsWith("META-INF/")) {
+            return true;
+        }
+        // Entries in subdirectories of META-INF must be listed in the manifest.
+        if (entryName.indexOf('/', "META-INF/".length()) != -1) {
+            return true;
+        }
+
+        // Ignored file names (case-insensitive) in META-INF directory:
+        //   MANIFEST.MF
+        //   *.SF
+        //   *.RSA
+        //   *.DSA
+        //   *.EC
+        //   SIG-*
+        String fileNameLowerCase =
+                entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
+        if (("manifest.mf".equals(fileNameLowerCase))
+                || (fileNameLowerCase.endsWith(".sf"))
+                || (fileNameLowerCase.endsWith(".rsa"))
+                || (fileNameLowerCase.endsWith(".dsa"))
+                || (fileNameLowerCase.endsWith(".ec"))
+                || (fileNameLowerCase.startsWith("sig-"))) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+     * JAR entries which need to be added to the APK as part of the signature.
+     *
+     * @param signerConfigs signer configurations, one for each signer. At least one signer config
+     *        must be provided.
+     *
+     * @throws ApkFormatException if the source manifest is malformed
+     * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+     *         missing
+     * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
+     *         cannot be used in general
+     * @throws SignatureException if an error occurs when computing digests of generating
+     *         signatures
+     */
+    public static List<Pair<String, byte[]>> sign(
+            List<SignerConfig> signerConfigs,
+            DigestAlgorithm jarEntryDigestAlgorithm,
+            Map<String, byte[]> jarEntryDigests,
+            List<Integer> apkSigningSchemeIds,
+            byte[] sourceManifestBytes,
+            String createdBy)
+                    throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
+                            CertificateException, SignatureException {
+        if (signerConfigs.isEmpty()) {
+            throw new IllegalArgumentException("At least one signer config must be provided");
+        }
+        if (signerConfigs.size() > MAX_APK_SIGNERS) {
+            throw new IllegalArgumentException(
+                    "APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", "
+                            + signerConfigs.size() + " provided");
+        }
+        OutputManifestFile manifest =
+                generateManifestFile(
+                        jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
+
+        return signManifest(
+                signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
+    }
+
+    /**
+     * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+     * JAR entries which need to be added to the APK as part of the signature.
+     *
+     * @param signerConfigs signer configurations, one for each signer. At least one signer config
+     *        must be provided.
+     *
+     * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
+     *         cannot be used in general
+     * @throws SignatureException if an error occurs when computing digests of generating
+     *         signatures
+     */
+    public static List<Pair<String, byte[]>> signManifest(
+            List<SignerConfig> signerConfigs,
+            DigestAlgorithm digestAlgorithm,
+            List<Integer> apkSigningSchemeIds,
+            String createdBy,
+            OutputManifestFile manifest)
+                    throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
+                            SignatureException {
+        if (signerConfigs.isEmpty()) {
+            throw new IllegalArgumentException("At least one signer config must be provided");
+        }
+
+        // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
+        List<Pair<String, byte[]>> signatureJarEntries =
+                new ArrayList<>(2 * signerConfigs.size() + 1);
+        byte[] sfBytes =
+                generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
+        for (SignerConfig signerConfig : signerConfigs) {
+            String signerName = signerConfig.name;
+            byte[] signatureBlock;
+            try {
+                signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeyException(
+                        "Failed to sign using signer \"" + signerName + "\"", e);
+            } catch (CertificateException e) {
+                throw new CertificateException(
+                        "Failed to sign using signer \"" + signerName + "\"", e);
+            } catch (SignatureException e) {
+                throw new SignatureException(
+                        "Failed to sign using signer \"" + signerName + "\"", e);
+            }
+            signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
+            PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+            String signatureBlockFileName =
+                    "META-INF/" + signerName + "."
+                            + publicKey.getAlgorithm().toUpperCase(Locale.US);
+            signatureJarEntries.add(
+                    Pair.of(signatureBlockFileName, signatureBlock));
+        }
+        signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
+        return signatureJarEntries;
+    }
+
+    /**
+     * Returns the names of JAR entries which this signer will produce as part of v1 signature.
+     */
+    public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
+        Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
+        for (SignerConfig signerConfig : signerConfigs) {
+            String signerName = signerConfig.name;
+            result.add("META-INF/" + signerName + ".SF");
+            PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+            String signatureBlockFileName =
+                    "META-INF/" + signerName + "."
+                            + publicKey.getAlgorithm().toUpperCase(Locale.US);
+            result.add(signatureBlockFileName);
+        }
+        result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
+        return result;
+    }
+
+    /**
+     * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
+     * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
+     */
+    public static OutputManifestFile generateManifestFile(
+            DigestAlgorithm jarEntryDigestAlgorithm,
+            Map<String, byte[]> jarEntryDigests,
+            byte[] sourceManifestBytes) throws ApkFormatException {
+        Manifest sourceManifest = null;
+        if (sourceManifestBytes != null) {
+            try {
+                sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
+            } catch (IOException e) {
+                throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
+            }
+        }
+        ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
+        Attributes mainAttrs = new Attributes();
+        // Copy the main section from the source manifest (if provided). Otherwise use defaults.
+        // NOTE: We don't output our own Created-By header because this signer did not create the
+        //       JAR/APK being signed -- the signer only adds signatures to the already existing
+        //       JAR/APK.
+        if (sourceManifest != null) {
+            mainAttrs.putAll(sourceManifest.getMainAttributes());
+        } else {
+            mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
+        }
+
+        try {
+            ManifestWriter.writeMainSection(manifestOut, mainAttrs);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+        }
+
+        List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
+        Collections.sort(sortedEntryNames);
+        SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
+        String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
+        for (String entryName : sortedEntryNames) {
+            checkEntryNameValid(entryName);
+            byte[] entryDigest = jarEntryDigests.get(entryName);
+            Attributes entryAttrs = new Attributes();
+            entryAttrs.putValue(
+                    entryDigestAttributeName,
+                    Base64.getEncoder().encodeToString(entryDigest));
+            ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
+            byte[] sectionBytes;
+            try {
+                ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
+                sectionBytes = sectionOut.toByteArray();
+                manifestOut.write(sectionBytes);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+            }
+            invidualSectionsContents.put(entryName, sectionBytes);
+        }
+
+        OutputManifestFile result = new OutputManifestFile();
+        result.contents = manifestOut.toByteArray();
+        result.mainSectionAttributes = mainAttrs;
+        result.individualSectionsContents = invidualSectionsContents;
+        return result;
+    }
+
+    private static void checkEntryNameValid(String name) throws ApkFormatException {
+        // JAR signing spec says CR, LF, and NUL are not permitted in entry names
+        // CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
+        // is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
+        // issues when parsing using C and C++ like languages.
+        for (char c : name.toCharArray()) {
+            if ((c == '\r') || (c == '\n') || (c == 0)) {
+                throw new ApkFormatException(
+                        String.format(
+                                "Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
+                                (int) c,
+                                name));
+            }
+        }
+    }
+
+    public static class OutputManifestFile {
+        public byte[] contents;
+        public SortedMap<String, byte[]> individualSectionsContents;
+        public Attributes mainSectionAttributes;
+    }
+
+    private static byte[] generateSignatureFile(
+            List<Integer> apkSignatureSchemeIds,
+            DigestAlgorithm manifestDigestAlgorithm,
+            String createdBy,
+            OutputManifestFile manifest) throws NoSuchAlgorithmException {
+        Manifest sf = new Manifest();
+        Attributes mainAttrs = sf.getMainAttributes();
+        mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
+        mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
+        if (!apkSignatureSchemeIds.isEmpty()) {
+            // Add APK Signature Scheme v2 (and newer) signature stripping protection.
+            // This attribute indicates that this APK is supposed to have been signed using one or
+            // more APK-specific signature schemes in addition to the standard JAR signature scheme
+            // used by this code. APK signature verifier should reject the APK if it does not
+            // contain a signature for the signature scheme the verifier prefers out of this set.
+            StringBuilder attrValue = new StringBuilder();
+            for (int id : apkSignatureSchemeIds) {
+                if (attrValue.length() > 0) {
+                    attrValue.append(", ");
+                }
+                attrValue.append(String.valueOf(id));
+            }
+            mainAttrs.put(
+                    SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
+                    attrValue.toString());
+        }
+
+        // Add main attribute containing the digest of MANIFEST.MF.
+        MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
+        mainAttrs.putValue(
+                getManifestDigestAttributeName(manifestDigestAlgorithm),
+                Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            SignatureFileWriter.writeMainSection(out, mainAttrs);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to write in-memory .SF file", e);
+        }
+        String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
+        for (Map.Entry<String, byte[]> manifestSection
+                : manifest.individualSectionsContents.entrySet()) {
+            String sectionName = manifestSection.getKey();
+            byte[] sectionContents = manifestSection.getValue();
+            byte[] sectionDigest = md.digest(sectionContents);
+            Attributes attrs = new Attributes();
+            attrs.putValue(
+                    entryDigestAttributeName,
+                    Base64.getEncoder().encodeToString(sectionDigest));
+
+            try {
+                SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write in-memory .SF file", e);
+            }
+        }
+
+        // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
+        // cause a spurious IOException to be thrown if the length of the signature file is a
+        // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
+        if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
+            try {
+                SignatureFileWriter.writeSectionDelimiter(out);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
+            }
+        }
+
+        return out.toByteArray();
+    }
+
+
+
+    /**
+     * Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
+     * signing configuration.
+     */
+    private static byte[] generateSignatureBlock(
+            SignerConfig signerConfig, byte[] signatureFileBytes)
+                    throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
+                            SignatureException {
+        // Obtain relevant bits of signing configuration
+        List<X509Certificate> signerCerts = signerConfig.certificates;
+        X509Certificate signingCert = signerCerts.get(0);
+        PublicKey publicKey = signingCert.getPublicKey();
+        DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
+        Pair<String, AlgorithmIdentifier> signatureAlgs =
+                getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
+                        signerConfig.deterministicDsaSigning);
+        String jcaSignatureAlgorithm = signatureAlgs.getFirst();
+
+        // Generate the cryptographic signature of the signature file
+        byte[] signatureBytes;
+        try {
+            Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+            signature.initSign(signerConfig.privateKey);
+            signature.update(signatureFileBytes);
+            signatureBytes = signature.sign();
+        } catch (InvalidKeyException e) {
+            throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
+        } catch (SignatureException e) {
+            throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
+        }
+
+        // Verify the signature against the public key in the signing certificate
+        try {
+            Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+            signature.initVerify(publicKey);
+            signature.update(signatureFileBytes);
+            if (!signature.verify(signatureBytes)) {
+                throw new SignatureException("Signature did not verify");
+            }
+        } catch (InvalidKeyException e) {
+            throw new InvalidKeyException(
+                    "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+                            + " public key from certificate",
+                    e);
+        } catch (SignatureException e) {
+            throw new SignatureException(
+                    "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+                            + " public key from certificate",
+                    e);
+        }
+
+        AlgorithmIdentifier digestAlgorithmId =
+                getSignerInfoDigestAlgorithmOid(digestAlgorithm);
+        AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
+        try {
+            return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
+                    signatureBytes,
+                    null,
+                    signerCerts, digestAlgorithmId,
+                    signatureAlgorithmId);
+        } catch (Asn1EncodingException | CertificateEncodingException ex) {
+            throw new SignatureException("Failed to encode signature block");
+        }
+    }
+
+
+
+    private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+        switch (digestAlgorithm) {
+            case SHA1:
+                return "SHA1-Digest";
+            case SHA256:
+                return "SHA-256-Digest";
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected content digest algorithm: " + digestAlgorithm);
+        }
+    }
+
+    private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+        switch (digestAlgorithm) {
+            case SHA1:
+                return "SHA1-Digest-Manifest";
+            case SHA256:
+                return "SHA-256-Digest-Manifest";
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected content digest algorithm: " + digestAlgorithm);
+        }
+    }
+}

+ 1570 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java

@@ -0,0 +1,1570 @@
+/*
+ * Copyright (C) 2016 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.apk.v1;
+
+import static com.android.apksig.Constants.MAX_APK_SIGNERS;
+import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm;
+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.Issue;
+import com.android.apksig.ApkVerifier.IssueWithParams;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.asn1.Asn1BerParser;
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1DecodingException;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.oid.OidConstants;
+import com.android.apksig.internal.pkcs7.Attribute;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+import com.android.apksig.internal.pkcs7.Pkcs7DecodingException;
+import com.android.apksig.internal.pkcs7.SignedData;
+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.InclusiveIntRange;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+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.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.jar.Attributes;
+
+/**
+ * APK verifier which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
+ */
+public abstract class V1SchemeVerifier {
+    private V1SchemeVerifier() {}
+
+    /**
+     * Verifies the provided APK's JAR signatures and returns the result of verification. APK is
+     * considered verified only if {@link Result#verified} is {@code true}. If verification fails,
+     * the result will contain errors -- see {@link Result#getErrors()}.
+     *
+     * <p>Verification succeeds iff the APK's JAR signatures are expected to verify on all Android
+     * platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. If the APK's signature
+     * is expected to not verify on any of the specified platform versions, this method returns a
+     * result with one or more errors and whose {@code Result.verified == false}, or this method
+     * throws an exception.
+     *
+     * @throws ApkFormatException if the APK is malformed
+     * @throws IOException if an I/O error occurs when reading the APK
+     * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a
+     *         required cryptographic algorithm implementation is missing
+     */
+    public static Result verify(
+            DataSource apk,
+            ApkUtils.ZipSections apkSections,
+            Map<Integer, String> supportedApkSigSchemeNames,
+            Set<Integer> foundApkSigSchemeIds,
+            int minSdkVersion,
+            int maxSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException {
+        if (minSdkVersion > maxSdkVersion) {
+            throw new IllegalArgumentException(
+                    "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion
+                            + ")");
+        }
+
+        Result result = new Result();
+
+        // Parse the ZIP Central Directory and check that there are no entries with duplicate names.
+        List<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections);
+        Set<String> cdEntryNames = checkForDuplicateEntries(cdRecords, result);
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Verify JAR signature(s).
+        Signers.verify(
+                apk,
+                apkSections.getZipCentralDirectoryOffset(),
+                cdRecords,
+                cdEntryNames,
+                supportedApkSigSchemeNames,
+                foundApkSigSchemeIds,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+
+        return result;
+    }
+
+    /**
+     * Returns the set of entry names and reports any duplicate entry names in the {@code result}
+     * as errors.
+     */
+    private static Set<String> checkForDuplicateEntries(
+            List<CentralDirectoryRecord> cdRecords, Result result) {
+        Set<String> cdEntryNames = new HashSet<>(cdRecords.size());
+        Set<String> duplicateCdEntryNames = null;
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            String entryName = cdRecord.getName();
+            if (!cdEntryNames.add(entryName)) {
+                // This is an error. Report this once per duplicate name.
+                if (duplicateCdEntryNames == null) {
+                    duplicateCdEntryNames = new HashSet<>();
+                }
+                if (duplicateCdEntryNames.add(entryName)) {
+                    result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName);
+                }
+            }
+        }
+        return cdEntryNames;
+    }
+
+    /**
+    * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section
+    * representation and a mapping between entry name and its manifest section representation.
+    *
+    * @param manifestBytes raw representation of Manifest.MF
+    * @param cdEntryNames expected set of entry names
+    * @param result object to keep track of errors that happened during the parsing
+    * @return a pair of main entry manifest section representation and a mapping between entry name
+    *     and its manifest section representation
+    */
+    public static Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> parseManifest(
+            byte[] manifestBytes, Set<String> cdEntryNames, Result result) {
+        ManifestParser manifest = new ManifestParser(manifestBytes);
+        ManifestParser.Section manifestMainSection = manifest.readSection();
+        List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections();
+        Map<String, ManifestParser.Section> entryNameToManifestSection =
+                new HashMap<>(manifestIndividualSections.size());
+        int manifestSectionNumber = 0;
+        for (ManifestParser.Section manifestSection : manifestIndividualSections) {
+            manifestSectionNumber++;
+            String entryName = manifestSection.getName();
+            if (entryName == null) {
+                result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber);
+                continue;
+            }
+            if (entryNameToManifestSection.put(entryName, manifestSection) != null) {
+                result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName);
+                continue;
+            }
+            if (!cdEntryNames.contains(entryName)) {
+                result.addError(
+                        Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName);
+                continue;
+            }
+        }
+        return Pair.of(manifestMainSection, entryNameToManifestSection);
+    }
+
+    /**
+     * All JAR signers of an APK.
+     */
+    private static class Signers {
+
+        /**
+         * Verifies JAR signatures of the provided APK and populates the provided result container
+         * with errors, warnings, and information about signers. The APK is considered verified if
+         * the {@link Result#verified} is {@code true}.
+         */
+        private static void verify(
+                DataSource apk,
+                long cdStartOffset,
+                List<CentralDirectoryRecord> cdRecords,
+                Set<String> cdEntryNames,
+                Map<Integer, String> supportedApkSigSchemeNames,
+                Set<Integer> foundApkSigSchemeIds,
+                int minSdkVersion,
+                int maxSdkVersion,
+                Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
+
+            // Find JAR manifest and signature block files.
+            CentralDirectoryRecord manifestEntry = null;
+            Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1);
+            List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1);
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                String entryName = cdRecord.getName();
+                if (!entryName.startsWith("META-INF/")) {
+                    continue;
+                }
+                if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(
+                        entryName))) {
+                    manifestEntry = cdRecord;
+                    continue;
+                }
+                if (entryName.endsWith(".SF")) {
+                    sigFileEntries.put(entryName, cdRecord);
+                    continue;
+                }
+                if ((entryName.endsWith(".RSA"))
+                        || (entryName.endsWith(".DSA"))
+                        || (entryName.endsWith(".EC"))) {
+                    sigBlockEntries.add(cdRecord);
+                    continue;
+                }
+            }
+            if (manifestEntry == null) {
+                result.addError(Issue.JAR_SIG_NO_MANIFEST);
+                return;
+            }
+
+            // Parse the JAR manifest and check that all JAR entries it references exist in the APK.
+            byte[] manifestBytes;
+            try {
+                manifestBytes =
+                        LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset);
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException("Malformed ZIP entry: " + manifestEntry.getName(), e);
+            }
+
+            Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> manifestSections =
+                    parseManifest(manifestBytes, cdEntryNames, result);
+
+            if (result.containsErrors()) {
+                return;
+            }
+
+            ManifestParser.Section manifestMainSection = manifestSections.getFirst();
+            Map<String, ManifestParser.Section> entryNameToManifestSection =
+                    manifestSections.getSecond();
+
+            // STATE OF AFFAIRS:
+            // * All JAR entries listed in JAR manifest are present in the APK.
+
+            // Identify signers
+            List<Signer> signers = new ArrayList<>(sigBlockEntries.size());
+            for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) {
+                String sigBlockEntryName = sigBlockEntry.getName();
+                int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.');
+                if (extensionDelimiterIndex == -1) {
+                    throw new RuntimeException(
+                            "Signature block file name does not contain extension: "
+                                    + sigBlockEntryName);
+                }
+                String sigFileEntryName =
+                        sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF";
+                CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName);
+                if (sigFileEntry == null) {
+                    result.addWarning(
+                            Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName);
+                    continue;
+                }
+                String signerName = sigBlockEntryName.substring("META-INF/".length());
+                Result.SignerInfo signerInfo =
+                        new Result.SignerInfo(
+                                signerName, sigBlockEntryName, sigFileEntry.getName());
+                Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo);
+                signers.add(signer);
+            }
+            if (signers.isEmpty()) {
+                result.addError(Issue.JAR_SIG_NO_SIGNATURES);
+                return;
+            }
+            if (signers.size() > MAX_APK_SIGNERS) {
+                result.addError(Issue.JAR_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS,
+                        signers.size());
+                return;
+            }
+
+            // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding
+            // signature file .SF. Any error encountered for any signer terminates verification, to
+            // mimic Android's behavior.
+            for (Signer signer : signers) {
+                signer.verifySigBlockAgainstSigFile(
+                        apk, cdStartOffset, minSdkVersion, maxSdkVersion);
+                if (signer.getResult().containsErrors()) {
+                    result.signers.add(signer.getResult());
+                }
+            }
+            if (result.containsErrors()) {
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All JAR entries listed in JAR manifest are present in the APK.
+            // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
+
+            // Verify each signer's signature file (.SF) against the JAR manifest.
+            List<Signer> remainingSigners = new ArrayList<>(signers.size());
+            for (Signer signer : signers) {
+                signer.verifySigFileAgainstManifest(
+                        manifestBytes,
+                        manifestMainSection,
+                        entryNameToManifestSection,
+                        supportedApkSigSchemeNames,
+                        foundApkSigSchemeIds,
+                        minSdkVersion,
+                        maxSdkVersion);
+                if (signer.isIgnored()) {
+                    result.ignoredSigners.add(signer.getResult());
+                } else {
+                    if (signer.getResult().containsErrors()) {
+                        result.signers.add(signer.getResult());
+                    } else {
+                        remainingSigners.add(signer);
+                    }
+                }
+            }
+            if (result.containsErrors()) {
+                return;
+            }
+            signers = remainingSigners;
+            if (signers.isEmpty()) {
+                result.addError(Issue.JAR_SIG_NO_SIGNATURES);
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
+            // * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
+            // * All JAR entries listed in JAR manifest are present in the APK.
+
+            // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's
+            // JAR entry is considered signed by signers associated with an .SF file iff the entry
+            // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest
+            // match theentry's uncompressed data. Android requires that all such JAR entries are
+            // signed by the same set of signers. This set may be smaller than the set of signers
+            // we've identified so far.
+            Set<Signer> apkSigners =
+                    verifyJarEntriesAgainstManifestAndSigners(
+                            apk,
+                            cdStartOffset,
+                            cdRecords,
+                            entryNameToManifestSection,
+                            signers,
+                            minSdkVersion,
+                            maxSdkVersion,
+                            result);
+            if (result.containsErrors()) {
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
+            // * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
+            // * All JAR entries listed in JAR manifest are present in the APK.
+            // * All JAR entries present in the APK and supposed to be covered by JAR signature
+            //   (i.e., reside outside of META-INF/) are covered by signatures from the same set
+            //   of signers.
+
+            // Report any JAR entries which aren't covered by signature.
+            Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2);
+            signatureEntryNames.add(manifestEntry.getName());
+            for (Signer signer : apkSigners) {
+                signatureEntryNames.add(signer.getSignatureBlockEntryName());
+                signatureEntryNames.add(signer.getSignatureFileEntryName());
+            }
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                String entryName = cdRecord.getName();
+                if ((entryName.startsWith("META-INF/"))
+                        && (!entryName.endsWith("/"))
+                        && (!signatureEntryNames.contains(entryName))) {
+                    result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName);
+                }
+            }
+
+            // Reflect the sets of used signers and ignored signers in the result.
+            for (Signer signer : signers) {
+                if (apkSigners.contains(signer)) {
+                    result.signers.add(signer.getResult());
+                } else {
+                    result.ignoredSigners.add(signer.getResult());
+                }
+            }
+
+            result.verified = true;
+        }
+    }
+
+    static class Signer {
+        private final String mName;
+        private final Result.SignerInfo mResult;
+        private final CentralDirectoryRecord mSignatureFileEntry;
+        private final CentralDirectoryRecord mSignatureBlockEntry;
+        private boolean mIgnored;
+
+        private byte[] mSigFileBytes;
+        private Set<String> mSigFileEntryNames;
+
+        private Signer(
+                String name,
+                CentralDirectoryRecord sigBlockEntry,
+                CentralDirectoryRecord sigFileEntry,
+                Result.SignerInfo result) {
+            mName = name;
+            mResult = result;
+            mSignatureBlockEntry = sigBlockEntry;
+            mSignatureFileEntry = sigFileEntry;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        public String getSignatureFileEntryName() {
+            return mSignatureFileEntry.getName();
+        }
+
+        public String getSignatureBlockEntryName() {
+            return mSignatureBlockEntry.getName();
+        }
+
+        void setIgnored() {
+            mIgnored = true;
+        }
+
+        public boolean isIgnored() {
+            return mIgnored;
+        }
+
+        public Set<String> getSigFileEntryNames() {
+            return mSigFileEntryNames;
+        }
+
+        public Result.SignerInfo getResult() {
+            return mResult;
+        }
+
+        public void verifySigBlockAgainstSigFile(
+                DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
+                        throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            // Obtain the signature block from the APK
+            byte[] sigBlockBytes;
+            try {
+                sigBlockBytes =
+                        LocalFileRecord.getUncompressedData(
+                                apk, mSignatureBlockEntry, cdStartOffset);
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException(
+                        "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e);
+            }
+            // Obtain the signature file from the APK
+            try {
+                mSigFileBytes =
+                        LocalFileRecord.getUncompressedData(
+                                apk, mSignatureFileEntry, cdStartOffset);
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException(
+                        "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e);
+            }
+
+            // Extract PKCS #7 SignedData from the signature block
+            SignedData signedData;
+            try {
+                ContentInfo contentInfo =
+                        Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class);
+                if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) {
+                    throw new Asn1DecodingException(
+                          "Unsupported ContentInfo.contentType: " + contentInfo.contentType);
+                }
+                signedData =
+                        Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class);
+            } catch (Asn1DecodingException e) {
+                e.printStackTrace();
+                mResult.addError(
+                        Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                return;
+            }
+
+            if (signedData.signerInfos.isEmpty()) {
+                mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
+                return;
+            }
+
+            // Find the first SignedData.SignerInfos element which verifies against the signature
+            // file
+            SignerInfo firstVerifiedSignerInfo = null;
+            X509Certificate firstVerifiedSignerInfoSigningCertificate = null;
+            // Prior to Android N, Android attempts to verify only the first SignerInfo. From N
+            // onwards, Android attempts to verify all SignerInfos and then picks the first verified
+            // SignerInfo.
+            List<SignerInfo> unverifiedSignerInfosToTry;
+            if (minSdkVersion < AndroidSdkVersion.N) {
+                unverifiedSignerInfosToTry =
+                        Collections.singletonList(signedData.signerInfos.get(0));
+            } else {
+                unverifiedSignerInfosToTry = signedData.signerInfos;
+            }
+            List<X509Certificate> signedDataCertificates = null;
+            for (SignerInfo unverifiedSignerInfo : unverifiedSignerInfosToTry) {
+                // Parse SignedData.certificates -- they are needed to verify SignerInfo
+                if (signedDataCertificates == null) {
+                    try {
+                        signedDataCertificates = parseCertificates(signedData.certificates);
+                    } catch (CertificateException e) {
+                        mResult.addError(
+                                Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                        return;
+                    }
+                }
+
+                // Verify SignerInfo
+                X509Certificate signingCertificate;
+                try {
+                    signingCertificate =
+                            verifySignerInfoAgainstSigFile(
+                                    signedData,
+                                    signedDataCertificates,
+                                    unverifiedSignerInfo,
+                                    mSigFileBytes,
+                                    minSdkVersion,
+                                    maxSdkVersion);
+                    if (mResult.containsErrors()) {
+                        return;
+                    }
+                    if (signingCertificate != null) {
+                        // SignerInfo verified
+                        if (firstVerifiedSignerInfo == null) {
+                            firstVerifiedSignerInfo = unverifiedSignerInfo;
+                            firstVerifiedSignerInfoSigningCertificate = signingCertificate;
+                        }
+                    }
+                } catch (Pkcs7DecodingException e) {
+                    mResult.addError(
+                            Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                    return;
+                } catch (InvalidKeyException | SignatureException e) {
+                    mResult.addError(
+                            Issue.JAR_SIG_VERIFY_EXCEPTION,
+                            mSignatureBlockEntry.getName(),
+                            mSignatureFileEntry.getName(),
+                            e);
+                    return;
+                }
+            }
+            if (firstVerifiedSignerInfo == null) {
+                // No SignerInfo verified
+                mResult.addError(
+                        Issue.JAR_SIG_DID_NOT_VERIFY,
+                        mSignatureBlockEntry.getName(),
+                        mSignatureFileEntry.getName());
+                return;
+            }
+            // Verified
+            List<X509Certificate> signingCertChain =
+                    getCertificateChain(
+                            signedDataCertificates, firstVerifiedSignerInfoSigningCertificate);
+            mResult.certChain.clear();
+            mResult.certChain.addAll(signingCertChain);
+        }
+
+        /**
+         * Returns the signing certificate if the provided {@link SignerInfo} verifies against the
+         * contents of the provided signature file, or {@code null} if it does not verify.
+         */
+        private X509Certificate verifySignerInfoAgainstSigFile(
+                SignedData signedData,
+                Collection<X509Certificate> signedDataCertificates,
+                SignerInfo signerInfo,
+                byte[] signatureFile,
+                int minSdkVersion,
+                int maxSdkVersion)
+                        throws Pkcs7DecodingException, NoSuchAlgorithmException,
+                                InvalidKeyException, SignatureException {
+            String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm;
+            String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm;
+            InclusiveIntRange desiredApiLevels =
+                    InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
+            List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
+                    getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
+            List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
+                    desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
+            if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
+                String digestAlgorithmUserFriendly =
+                        OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+                                digestAlgorithmOid);
+                if (digestAlgorithmUserFriendly == null) {
+                    digestAlgorithmUserFriendly = digestAlgorithmOid;
+                }
+                String signatureAlgorithmUserFriendly =
+                        OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+                                signatureAlgorithmOid);
+                if (signatureAlgorithmUserFriendly == null) {
+                    signatureAlgorithmUserFriendly = signatureAlgorithmOid;
+                }
+                StringBuilder apiLevelsUserFriendly = new StringBuilder();
+                for (InclusiveIntRange range : apiLevelsWhereDigestAlgorithmNotSupported) {
+                    if (apiLevelsUserFriendly.length() > 0) {
+                        apiLevelsUserFriendly.append(", ");
+                    }
+                    if (range.getMin() == range.getMax()) {
+                        apiLevelsUserFriendly.append(String.valueOf(range.getMin()));
+                    } else if (range.getMax() == Integer.MAX_VALUE) {
+                        apiLevelsUserFriendly.append(range.getMin() + "+");
+                    } else {
+                        apiLevelsUserFriendly.append(range.getMin() + "-" + range.getMax());
+                    }
+                }
+                mResult.addError(
+                        Issue.JAR_SIG_UNSUPPORTED_SIG_ALG,
+                        mSignatureBlockEntry.getName(),
+                        digestAlgorithmOid,
+                        signatureAlgorithmOid,
+                        apiLevelsUserFriendly.toString(),
+                        digestAlgorithmUserFriendly,
+                        signatureAlgorithmUserFriendly);
+                return null;
+            }
+
+            // From the bag of certs, obtain the certificate referenced by the SignerInfo,
+            // and verify the cryptographic signature in the SignerInfo against the certificate.
+
+            // Locate the signing certificate referenced by the SignerInfo
+            X509Certificate signingCertificate =
+                    findCertificate(signedDataCertificates, signerInfo.sid);
+            if (signingCertificate == null) {
+                throw new SignatureException(
+                        "Signing certificate referenced in SignerInfo not found in"
+                                + " SignedData");
+            }
+
+            // Check whether the signing certificate is acceptable. Android performs these
+            // checks explicitly, instead of delegating this to
+            // Signature.initVerify(Certificate).
+            if (signingCertificate.hasUnsupportedCriticalExtension()) {
+                throw new SignatureException(
+                        "Signing certificate has unsupported critical extensions");
+            }
+            boolean[] keyUsageExtension = signingCertificate.getKeyUsage();
+            if (keyUsageExtension != null) {
+                boolean digitalSignature =
+                        (keyUsageExtension.length >= 1) && (keyUsageExtension[0]);
+                boolean nonRepudiation =
+                        (keyUsageExtension.length >= 2) && (keyUsageExtension[1]);
+                if ((!digitalSignature) && (!nonRepudiation)) {
+                    throw new SignatureException(
+                            "Signing certificate not authorized for use in digital signatures"
+                                    + ": keyUsage extension missing digitalSignature and"
+                                    + " nonRepudiation");
+                }
+            }
+
+            // Verify the cryptographic signature in SignerInfo against the certificate's
+            // public key
+            String jcaSignatureAlgorithm =
+                    getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid);
+            Signature s = Signature.getInstance(jcaSignatureAlgorithm);
+            PublicKey publicKey = signingCertificate.getPublicKey();
+            try {
+                s.initVerify(publicKey);
+            } catch (InvalidKeyException e) {
+                // An InvalidKeyException could be caught if the PublicKey in the certificate is not
+                // properly encoded; attempt to resolve any encoding errors, generate a new public
+                // key, and reattempt the initVerify with the newly encoded key.
+                try {
+                    byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey);
+                    publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic(
+                            new X509EncodedKeySpec(encodedPublicKey));
+                } catch (InvalidKeySpecException ikse) {
+                    // If an InvalidKeySpecException is caught then throw the original Exception
+                    // since the key couldn't be properly re-encoded, and the original Exception
+                    // will have more useful debugging info.
+                    throw e;
+                }
+                s = Signature.getInstance(jcaSignatureAlgorithm);
+                s.initVerify(publicKey);
+            }
+
+            if (signerInfo.signedAttrs != null) {
+                // Signed attributes present -- verify signature against the ASN.1 DER encoded form
+                // of signed attributes. This verifies integrity of the signature file because
+                // signed attributes must contain the digest of the signature file.
+                if (minSdkVersion < AndroidSdkVersion.KITKAT) {
+                    // Prior to Android KitKat, APKs with signed attributes are unsafe:
+                    // * The APK's contents are not protected by the JAR signature because the
+                    //   digest in signed attributes is not verified. This means an attacker can
+                    //   arbitrarily modify the APK without invalidating its signature.
+                    // * Luckily, the signature over signed attributes was verified incorrectly
+                    //   (over the verbatim IMPLICIT [0] form rather than over re-encoded
+                    //   UNIVERSAL SET form) which means that JAR signatures which would verify on
+                    //   pre-KitKat Android and yet do not protect the APK from modification could
+                    //   be generated only by broken tools or on purpose by the entity signing the
+                    //   APK.
+                    //
+                    // We thus reject such unsafe APKs, even if they verify on platforms before
+                    // KitKat.
+                    throw new SignatureException(
+                            "APKs with Signed Attributes broken on platforms with API Level < "
+                                    + AndroidSdkVersion.KITKAT);
+                }
+                try {
+                    List<Attribute> signedAttributes =
+                            Asn1BerParser.parseImplicitSetOf(
+                                    signerInfo.signedAttrs.getEncoded(), Attribute.class);
+                    SignedAttributes signedAttrs = new SignedAttributes(signedAttributes);
+                    if (maxSdkVersion >= AndroidSdkVersion.N) {
+                        // Content Type attribute is checked only on Android N and newer
+                        String contentType =
+                                signedAttrs.getSingleObjectIdentifierValue(
+                                        Pkcs7Constants.OID_CONTENT_TYPE);
+                        if (contentType == null) {
+                            throw new SignatureException("No Content Type in signed attributes");
+                        }
+                        if (!contentType.equals(signedData.encapContentInfo.contentType)) {
+                            // Did not verify: Content type signed attribute does not match
+                            // SignedData.encapContentInfo.eContentType. This fails verification of
+                            // this SignerInfo but should not prevent verification of other
+                            // SignerInfos. Hence, no exception is thrown.
+                            return null;
+                        }
+                    }
+                    byte[] expectedSignatureFileDigest =
+                            signedAttrs.getSingleOctetStringValue(
+                                    Pkcs7Constants.OID_MESSAGE_DIGEST);
+                    if (expectedSignatureFileDigest == null) {
+                        throw new SignatureException("No content digest in signed attributes");
+                    }
+                    byte[] actualSignatureFileDigest =
+                            MessageDigest.getInstance(
+                                    getJcaDigestAlgorithm(digestAlgorithmOid))
+                                    .digest(signatureFile);
+                    if (!Arrays.equals(
+                            expectedSignatureFileDigest, actualSignatureFileDigest)) {
+                        // Skip verification: signature file digest in signed attributes does not
+                        // match the signature file. This fails verification of
+                        // this SignerInfo but should not prevent verification of other
+                        // SignerInfos. Hence, no exception is thrown.
+                        return null;
+                    }
+                } catch (Asn1DecodingException e) {
+                    throw new SignatureException("Failed to parse signed attributes", e);
+                }
+                // PKCS #7 requires that signature is over signed attributes re-encoded as
+                // ASN.1 DER. However, Android does not re-encode except for changing the
+                // first byte of encoded form from IMPLICIT [0] to UNIVERSAL SET. We do the
+                // same for maximum compatibility.
+                ByteBuffer signedAttrsOriginalEncoding = signerInfo.signedAttrs.getEncoded();
+                s.update((byte) 0x31); // UNIVERSAL SET
+                signedAttrsOriginalEncoding.position(1);
+                s.update(signedAttrsOriginalEncoding);
+            } else {
+                // No signed attributes present -- verify signature against the contents of the
+                // signature file
+                s.update(signatureFile);
+            }
+            byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice());
+            if (!s.verify(sigBytes)) {
+                // Cryptographic signature did not verify. This fails verification of this
+                // SignerInfo but should not prevent verification of other SignerInfos. Hence, no
+                // exception is thrown.
+                return null;
+            }
+            // Cryptographic signature verified
+            return signingCertificate;
+        }
+
+
+
+        public static List<X509Certificate> getCertificateChain(
+                List<X509Certificate> certs, X509Certificate leaf) {
+            List<X509Certificate> unusedCerts = new ArrayList<>(certs);
+            List<X509Certificate> result = new ArrayList<>(1);
+            result.add(leaf);
+            unusedCerts.remove(leaf);
+            X509Certificate root = leaf;
+            while (!root.getSubjectDN().equals(root.getIssuerDN())) {
+                Principal targetDn = root.getIssuerDN();
+                boolean issuerFound = false;
+                for (int i = 0; i < unusedCerts.size(); i++) {
+                    X509Certificate unusedCert = unusedCerts.get(i);
+                    if (targetDn.equals(unusedCert.getSubjectDN())) {
+                        issuerFound = true;
+                        unusedCerts.remove(i);
+                        result.add(unusedCert);
+                        root = unusedCert;
+                        break;
+                    }
+                }
+                if (!issuerFound) {
+                    break;
+                }
+            }
+            return result;
+        }
+
+
+
+
+        public void verifySigFileAgainstManifest(
+                byte[] manifestBytes,
+                ManifestParser.Section manifestMainSection,
+                Map<String, ManifestParser.Section> entryNameToManifestSection,
+                Map<Integer, String> supportedApkSigSchemeNames,
+                Set<Integer> foundApkSigSchemeIds,
+                int minSdkVersion,
+                int maxSdkVersion) throws NoSuchAlgorithmException {
+            // Inspect the main section of the .SF file.
+            ManifestParser sf = new ManifestParser(mSigFileBytes);
+            ManifestParser.Section sfMainSection = sf.readSection();
+            if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) {
+                mResult.addError(
+                        Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE,
+                        mSignatureFileEntry.getName());
+                setIgnored();
+                return;
+            }
+
+            if (maxSdkVersion >= AndroidSdkVersion.N) {
+                // Android N and newer rejects APKs whose .SF file says they were supposed to be
+                // signed with APK Signature Scheme v2 (or newer) and yet no such signature was
+                // found.
+                checkForStrippedApkSignatures(
+                        sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
+                if (mResult.containsErrors()) {
+                    return;
+                }
+            }
+
+            boolean createdBySigntool = false;
+            String createdBy = sfMainSection.getAttributeValue("Created-By");
+            if (createdBy != null) {
+                createdBySigntool = createdBy.indexOf("signtool") != -1;
+            }
+            boolean manifestDigestVerified =
+                    verifyManifestDigest(
+                            sfMainSection,
+                            createdBySigntool,
+                            manifestBytes,
+                            minSdkVersion,
+                            maxSdkVersion);
+            if (!createdBySigntool) {
+                verifyManifestMainSectionDigest(
+                        sfMainSection,
+                        manifestMainSection,
+                        manifestBytes,
+                        minSdkVersion,
+                        maxSdkVersion);
+            }
+            if (mResult.containsErrors()) {
+                return;
+            }
+
+            // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest
+            // verifies, per-entry sections should be ignored. However, most Android platform
+            // implementations require that such sections exist.
+            List<ManifestParser.Section> sfSections = sf.readAllSections();
+            Set<String> sfEntryNames = new HashSet<>(sfSections.size());
+            int sfSectionNumber = 0;
+            for (ManifestParser.Section sfSection : sfSections) {
+                sfSectionNumber++;
+                String entryName = sfSection.getName();
+                if (entryName == null) {
+                    mResult.addError(
+                            Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION,
+                            mSignatureFileEntry.getName(),
+                            sfSectionNumber);
+                    setIgnored();
+                    return;
+                }
+                if (!sfEntryNames.add(entryName)) {
+                    mResult.addError(
+                            Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION,
+                            mSignatureFileEntry.getName(),
+                            entryName);
+                    setIgnored();
+                    return;
+                }
+                if (manifestDigestVerified) {
+                    // No need to verify this entry's corresponding JAR manifest entry because the
+                    // JAR manifest verifies in full.
+                    continue;
+                }
+                // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify
+                // the digest of the JAR manifest section corresponding to this .SF section.
+                ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
+                if (manifestSection == null) {
+                    mResult.addError(
+                            Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
+                            entryName,
+                            mSignatureFileEntry.getName());
+                    setIgnored();
+                    continue;
+                }
+                verifyManifestIndividualSectionDigest(
+                        sfSection,
+                        createdBySigntool,
+                        manifestSection,
+                        manifestBytes,
+                        minSdkVersion,
+                        maxSdkVersion);
+            }
+            mSigFileEntryNames = sfEntryNames;
+        }
+
+
+        /**
+         * Returns {@code true} if the whole-file digest of the manifest against the main section of
+         * the .SF file.
+         */
+        private boolean verifyManifestDigest(
+                ManifestParser.Section sfMainSection,
+                boolean createdBySigntool,
+                byte[] manifestBytes,
+                int minSdkVersion,
+                int maxSdkVersion) throws NoSuchAlgorithmException {
+            Collection<NamedDigest> expectedDigests =
+                    getDigestsToVerify(
+                            sfMainSection,
+                            ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"),
+                            minSdkVersion,
+                            maxSdkVersion);
+            boolean digestFound = !expectedDigests.isEmpty();
+            if (!digestFound) {
+                mResult.addWarning(
+                        Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE,
+                        mSignatureFileEntry.getName());
+                return false;
+            }
+
+            boolean verified = true;
+            for (NamedDigest expectedDigest : expectedDigests) {
+                String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
+                byte[] actual = digest(jcaDigestAlgorithm, manifestBytes);
+                byte[] expected = expectedDigest.digest;
+                if (!Arrays.equals(expected, actual)) {
+                    mResult.addWarning(
+                            Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
+                            V1SchemeConstants.MANIFEST_ENTRY_NAME,
+                            jcaDigestAlgorithm,
+                            mSignatureFileEntry.getName(),
+                            Base64.getEncoder().encodeToString(actual),
+                            Base64.getEncoder().encodeToString(expected));
+                    verified = false;
+                }
+            }
+            return verified;
+        }
+
+        /**
+         * Verifies the digest of the manifest's main section against the main section of the .SF
+         * file.
+         */
+        private void verifyManifestMainSectionDigest(
+                ManifestParser.Section sfMainSection,
+                ManifestParser.Section manifestMainSection,
+                byte[] manifestBytes,
+                int minSdkVersion,
+                int maxSdkVersion) throws NoSuchAlgorithmException {
+            Collection<NamedDigest> expectedDigests =
+                    getDigestsToVerify(
+                            sfMainSection,
+                            "-Digest-Manifest-Main-Attributes",
+                            minSdkVersion,
+                            maxSdkVersion);
+            if (expectedDigests.isEmpty()) {
+                return;
+            }
+
+            for (NamedDigest expectedDigest : expectedDigests) {
+                String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
+                byte[] actual =
+                        digest(
+                                jcaDigestAlgorithm,
+                                manifestBytes,
+                                manifestMainSection.getStartOffset(),
+                                manifestMainSection.getSizeBytes());
+                byte[] expected = expectedDigest.digest;
+                if (!Arrays.equals(expected, actual)) {
+                    mResult.addError(
+                            Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY,
+                            jcaDigestAlgorithm,
+                            mSignatureFileEntry.getName(),
+                            Base64.getEncoder().encodeToString(actual),
+                            Base64.getEncoder().encodeToString(expected));
+                }
+            }
+        }
+
+        /**
+         * Verifies the digest of the manifest's individual section against the corresponding
+         * individual section of the .SF file.
+         */
+        private void verifyManifestIndividualSectionDigest(
+                ManifestParser.Section sfIndividualSection,
+                boolean createdBySigntool,
+                ManifestParser.Section manifestIndividualSection,
+                byte[] manifestBytes,
+                int minSdkVersion,
+                int maxSdkVersion) throws NoSuchAlgorithmException {
+            String entryName = sfIndividualSection.getName();
+            Collection<NamedDigest> expectedDigests =
+                    getDigestsToVerify(
+                            sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion);
+            if (expectedDigests.isEmpty()) {
+                mResult.addError(
+                        Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
+                        entryName,
+                        mSignatureFileEntry.getName());
+                return;
+            }
+
+            int sectionStartIndex = manifestIndividualSection.getStartOffset();
+            int sectionSizeBytes = manifestIndividualSection.getSizeBytes();
+            if (createdBySigntool) {
+                int sectionEndIndex = sectionStartIndex + sectionSizeBytes;
+                if ((manifestBytes[sectionEndIndex - 1] == '\n')
+                        && (manifestBytes[sectionEndIndex - 2] == '\n')) {
+                    sectionSizeBytes--;
+                }
+            }
+            for (NamedDigest expectedDigest : expectedDigests) {
+                String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
+                byte[] actual =
+                        digest(
+                                jcaDigestAlgorithm,
+                                manifestBytes,
+                                sectionStartIndex,
+                                sectionSizeBytes);
+                byte[] expected = expectedDigest.digest;
+                if (!Arrays.equals(expected, actual)) {
+                    mResult.addError(
+                            Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY,
+                            entryName,
+                            jcaDigestAlgorithm,
+                            mSignatureFileEntry.getName(),
+                            Base64.getEncoder().encodeToString(actual),
+                            Base64.getEncoder().encodeToString(expected));
+                }
+            }
+        }
+
+        private void checkForStrippedApkSignatures(
+                ManifestParser.Section sfMainSection,
+                Map<Integer, String> supportedApkSigSchemeNames,
+                Set<Integer> foundApkSigSchemeIds) {
+            String signedWithApkSchemes =
+                    sfMainSection.getAttributeValue(
+                            V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+            // This field contains a comma-separated list of APK signature scheme IDs which were
+            // used to sign this APK. Android rejects APKs where an ID is known to the platform but
+            // the APK didn't verify using that scheme.
+
+            if (signedWithApkSchemes == null) {
+                // APK signature (e.g., v2 scheme) stripping protections not enabled.
+                if (!foundApkSigSchemeIds.isEmpty()) {
+                    // APK is signed with an APK signature scheme such as v2 scheme.
+                    mResult.addWarning(
+                            Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION,
+                            mSignatureFileEntry.getName());
+                }
+                return;
+            }
+
+            if (supportedApkSigSchemeNames.isEmpty()) {
+                return;
+            }
+
+            Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
+            Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
+            StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ",");
+            while (tokenizer.hasMoreTokens()) {
+                String idText = tokenizer.nextToken().trim();
+                if (idText.isEmpty()) {
+                    continue;
+                }
+                int id;
+                try {
+                    id = Integer.parseInt(idText);
+                } catch (Exception ignored) {
+                    continue;
+                }
+                // This APK was supposed to be signed with the APK signature scheme having
+                // this ID.
+                if (supportedApkSigSchemeIds.contains(id)) {
+                    supportedExpectedApkSigSchemeIds.add(id);
+                } else {
+                    mResult.addWarning(
+                            Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID,
+                            mSignatureFileEntry.getName(),
+                            id);
+                }
+            }
+
+            for (int id : supportedExpectedApkSigSchemeIds) {
+                if (!foundApkSigSchemeIds.contains(id)) {
+                    String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
+                    mResult.addError(
+                            Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED,
+                            mSignatureFileEntry.getName(),
+                            id,
+                            apkSigSchemeName);
+                }
+            }
+        }
+    }
+
+    public static Collection<NamedDigest> getDigestsToVerify(
+            ManifestParser.Section section,
+            String digestAttrSuffix,
+            int minSdkVersion,
+            int maxSdkVersion) {
+        Decoder base64Decoder = Base64.getDecoder();
+        List<NamedDigest> result = new ArrayList<>(1);
+        if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) {
+            // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is
+            // to rely on the ancient Digest-Algorithms attribute which contains
+            // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The
+            // first digest attribute (with supported digest algorithm) found using the list is
+            // used.
+            String algs = section.getAttributeValue("Digest-Algorithms");
+            if (algs == null) {
+                algs = "SHA SHA1";
+            }
+            StringTokenizer tokens = new StringTokenizer(algs);
+            while (tokens.hasMoreTokens()) {
+                String alg = tokens.nextToken();
+                String attrName = alg + digestAttrSuffix;
+                String digestBase64 = section.getAttributeValue(attrName);
+                if (digestBase64 == null) {
+                    // Attribute not found
+                    continue;
+                }
+                alg = getCanonicalJcaMessageDigestAlgorithm(alg);
+                if ((alg == null)
+                        || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg)
+                                > minSdkVersion)) {
+                    // Unsupported digest algorithm
+                    continue;
+                }
+                // Supported digest algorithm
+                result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64)));
+                break;
+            }
+            // No supported digests found -- this will fail to verify on pre-JB MR2 Androids.
+            if (result.isEmpty()) {
+                return result;
+            }
+        }
+
+        if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) {
+            // On JB MR2 and newer, Android platform picks the strongest algorithm out of:
+            // SHA-512, SHA-384, SHA-256, SHA-1.
+            for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
+                String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
+                String digestBase64 = section.getAttributeValue(attrName);
+                if (digestBase64 == null) {
+                    // Attribute not found
+                    continue;
+                }
+                byte[] digest = base64Decoder.decode(digestBase64);
+                byte[] digestInResult = getDigest(result, alg);
+                if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) {
+                    result.add(new NamedDigest(alg, digest));
+                }
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = {
+            "SHA-512",
+            "SHA-384",
+            "SHA-256",
+            "SHA-1",
+    };
+
+    private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) {
+        return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US));
+    }
+
+    public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(
+            String jcaAlgorithmName) {
+        Integer result =
+                MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get(
+                        jcaAlgorithmName.toUpperCase(Locale.US));
+        return (result != null) ? result : Integer.MAX_VALUE;
+    }
+
+    private static String getJarDigestAttributeName(
+            String jcaDigestAlgorithm, String attrNameSuffix) {
+        if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) {
+            return "SHA1" + attrNameSuffix;
+        } else {
+            return jcaDigestAlgorithm + attrNameSuffix;
+        }
+    }
+
+    private static final Map<String, String> UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL;
+    static {
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8);
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512");
+    }
+
+    private static final Map<String, Integer>
+            MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST;
+    static {
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put(
+                "SHA-384", AndroidSdkVersion.GINGERBREAD);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put(
+                "SHA-512", AndroidSdkVersion.GINGERBREAD);
+    }
+
+    private static byte[] getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm) {
+        for (NamedDigest digest : digests) {
+            if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) {
+                return digest.digest;
+            }
+        }
+        return null;
+    }
+
+    public static List<CentralDirectoryRecord> parseZipCentralDirectory(
+            DataSource apk,
+            ApkUtils.ZipSections apkSections)
+                    throws IOException, ApkFormatException {
+        return ZipUtils.parseZipCentralDirectory(apk, apkSections);
+    }
+
+    /**
+     * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
+     * manifest for the APK to verify on Android.
+     */
+    private static boolean isJarEntryDigestNeededInManifest(String entryName) {
+        // NOTE: This logic is different from what's required by the JAR signing scheme. This is
+        // because Android's APK verification logic differs from that spec. In particular, JAR
+        // signing spec includes into JAR manifest all files in subdirectories of META-INF and
+        // any files inside META-INF not related to signatures.
+        if (entryName.startsWith("META-INF/")) {
+            return false;
+        }
+        return !entryName.endsWith("/");
+    }
+
+    private static Set<Signer> verifyJarEntriesAgainstManifestAndSigners(
+            DataSource apk,
+            long cdOffsetInApk,
+            Collection<CentralDirectoryRecord> cdRecords,
+            Map<String, ManifestParser.Section> entryNameToManifestSection,
+            List<Signer> signers,
+            int minSdkVersion,
+            int maxSdkVersion,
+            Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
+        // Iterate over APK contents as sequentially as possible to improve performance.
+        List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset =
+                new ArrayList<>(cdRecords);
+        Collections.sort(
+                cdRecordsSortedByLocalFileHeaderOffset,
+                CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+        List<Signer> firstSignedEntrySigners = null;
+        String firstSignedEntryName = null;
+        for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
+            String entryName = cdRecord.getName();
+            if (!isJarEntryDigestNeededInManifest(entryName)) {
+                continue;
+            }
+
+            ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
+            if (manifestSection == null) {
+                result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
+                continue;
+            }
+
+            List<Signer> entrySigners = new ArrayList<>(signers.size());
+            for (Signer signer : signers) {
+                if (signer.getSigFileEntryNames().contains(entryName)) {
+                    entrySigners.add(signer);
+                }
+            }
+            if (entrySigners.isEmpty()) {
+                result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName);
+                continue;
+            }
+            if (firstSignedEntrySigners == null) {
+                firstSignedEntrySigners = entrySigners;
+                firstSignedEntryName = entryName;
+            } else if (!entrySigners.equals(firstSignedEntrySigners)) {
+                result.addError(
+                        Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH,
+                        firstSignedEntryName,
+                        getSignerNames(firstSignedEntrySigners),
+                        entryName,
+                        getSignerNames(entrySigners));
+                continue;
+            }
+
+            List<NamedDigest> expectedDigests =
+                    new ArrayList<>(
+                            getDigestsToVerify(
+                                    manifestSection, "-Digest", minSdkVersion, maxSdkVersion));
+            if (expectedDigests.isEmpty()) {
+                result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
+                continue;
+            }
+
+            MessageDigest[] mds = new MessageDigest[expectedDigests.size()];
+            for (int i = 0; i < expectedDigests.size(); i++) {
+                mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm);
+            }
+
+            try {
+                LocalFileRecord.outputUncompressedData(
+                        apk,
+                        cdRecord,
+                        cdOffsetInApk,
+                        DataSinks.asDataSink(mds));
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException("Malformed ZIP entry: " + entryName, e);
+            } catch (IOException e) {
+                throw new IOException("Failed to read entry: " + entryName, e);
+            }
+
+            for (int i = 0; i < expectedDigests.size(); i++) {
+                NamedDigest expectedDigest = expectedDigests.get(i);
+                byte[] actualDigest = mds[i].digest();
+                if (!Arrays.equals(expectedDigest.digest, actualDigest)) {
+                    result.addError(
+                            Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
+                            entryName,
+                            expectedDigest.jcaDigestAlgorithm,
+                            V1SchemeConstants.MANIFEST_ENTRY_NAME,
+                            Base64.getEncoder().encodeToString(actualDigest),
+                            Base64.getEncoder().encodeToString(expectedDigest.digest));
+                }
+            }
+        }
+
+        if (firstSignedEntrySigners == null) {
+            result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES);
+            return Collections.emptySet();
+        } else {
+            return new HashSet<>(firstSignedEntrySigners);
+        }
+    }
+
+    private static List<String> getSignerNames(List<Signer> signers) {
+        if (signers.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<String> result = new ArrayList<>(signers.size());
+        for (Signer signer : signers) {
+            result.add(signer.getName());
+        }
+        return result;
+    }
+
+    private static MessageDigest getMessageDigest(String algorithm)
+            throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance(algorithm);
+    }
+
+    private static byte[] digest(String algorithm, byte[] data, int offset, int length)
+            throws NoSuchAlgorithmException {
+        MessageDigest md = getMessageDigest(algorithm);
+        md.update(data, offset, length);
+        return md.digest();
+    }
+
+    private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException {
+        return getMessageDigest(algorithm).digest(data);
+    }
+
+    public static class NamedDigest {
+        public final String jcaDigestAlgorithm;
+        public final byte[] digest;
+
+        private NamedDigest(String jcaDigestAlgorithm, byte[] digest) {
+            this.jcaDigestAlgorithm = jcaDigestAlgorithm;
+            this.digest = digest;
+        }
+    }
+
+    public static class Result {
+
+        /** Whether the APK's JAR signature verifies. */
+        public boolean verified;
+
+        /** List of APK's signers. These signers are used by Android. */
+        public final List<SignerInfo> signers = new ArrayList<>();
+
+        /**
+         * Signers encountered in the APK but not included in the set of the APK's signers. These
+         * signers are ignored by Android.
+         */
+        public final List<SignerInfo> ignoredSigners = new ArrayList<>();
+
+        private final List<IssueWithParams> mWarnings = new ArrayList<>();
+        private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+        private boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            for (SignerInfo signer : signers) {
+                if (signer.containsErrors()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void addError(Issue msg, Object... parameters) {
+            mErrors.add(new IssueWithParams(msg, parameters));
+        }
+
+        private void addWarning(Issue msg, Object... parameters) {
+            mWarnings.add(new IssueWithParams(msg, parameters));
+        }
+
+        public List<IssueWithParams> getErrors() {
+            return mErrors;
+        }
+
+        public List<IssueWithParams> getWarnings() {
+            return mWarnings;
+        }
+
+        public static class SignerInfo {
+            public final String name;
+            public final String signatureFileName;
+            public final String signatureBlockFileName;
+            public final List<X509Certificate> certChain = new ArrayList<>();
+
+            private final List<IssueWithParams> mWarnings = new ArrayList<>();
+            private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+            private SignerInfo(
+                    String name, String signatureBlockFileName, String signatureFileName) {
+                this.name = name;
+                this.signatureBlockFileName = signatureBlockFileName;
+                this.signatureFileName = signatureFileName;
+            }
+
+            private boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            private void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+
+            private void addWarning(Issue msg, Object... parameters) {
+                mWarnings.add(new IssueWithParams(msg, parameters));
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+        }
+    }
+
+    private static class SignedAttributes {
+        private Map<String, List<Asn1OpaqueObject>> mAttrs;
+
+        public SignedAttributes(Collection<Attribute> attrs) throws Pkcs7DecodingException {
+            Map<String, List<Asn1OpaqueObject>> result = new HashMap<>(attrs.size());
+            for (Attribute attr : attrs) {
+                if (result.put(attr.attrType, attr.attrValues) != null) {
+                    throw new Pkcs7DecodingException("Duplicate signed attribute: " + attr.attrType);
+                }
+            }
+            mAttrs = result;
+        }
+
+        private Asn1OpaqueObject getSingleValue(String attrOid) throws Pkcs7DecodingException {
+            List<Asn1OpaqueObject> values = mAttrs.get(attrOid);
+            if ((values == null) || (values.isEmpty())) {
+                return null;
+            }
+            if (values.size() > 1) {
+                throw new Pkcs7DecodingException("Attribute " + attrOid + " has multiple values");
+            }
+            return values.get(0);
+        }
+
+        public String getSingleObjectIdentifierValue(String attrOid) throws Pkcs7DecodingException {
+            Asn1OpaqueObject value = getSingleValue(attrOid);
+            if (value == null) {
+                return null;
+            }
+            try {
+                return Asn1BerParser.parse(value.getEncoded(), ObjectIdentifierChoice.class).value;
+            } catch (Asn1DecodingException e) {
+                throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e);
+            }
+        }
+
+        public byte[] getSingleOctetStringValue(String attrOid) throws Pkcs7DecodingException {
+            Asn1OpaqueObject value = getSingleValue(attrOid);
+            if (value == null) {
+                return null;
+            }
+            try {
+                return Asn1BerParser.parse(value.getEncoded(), OctetStringChoice.class).value;
+            } catch (Asn1DecodingException e) {
+                throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e);
+            }
+        }
+    }
+
+    @Asn1Class(type = Asn1Type.CHOICE)
+    public static class OctetStringChoice {
+        @Asn1Field(type = Asn1Type.OCTET_STRING)
+        public byte[] value;
+    }
+
+    @Asn1Class(type = Asn1Type.CHOICE)
+    public static class ObjectIdentifierChoice {
+        @Asn1Field(type = Asn1Type.OBJECT_IDENTIFIER)
+        public String value;
+    }
+}

+ 25 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 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.apk.v2;
+
+/** Constants used by the V2 Signature Scheme signing and verification. */
+public class V2SchemeConstants {
+    private V2SchemeConstants() {}
+
+    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+    public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
+}

+ 329 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java

@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2016 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.apk.v2;
+
+import static com.android.apksig.Constants.MAX_APK_SIGNERS;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
+
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK Signature Scheme v2 signer.
+ *
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ */
+public abstract class V2SchemeSigner {
+    /*
+     * The two main goals of APK Signature Scheme v2 are:
+     * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
+     *    cover every byte of the APK being signed.
+     * 2. Enable much faster signature and integrity verification. This is achieved by requiring
+     *    only a minimal amount of APK parsing before the signature is verified, thus completely
+     *    bypassing ZIP entry decompression and by making integrity verification parallelizable by
+     *    employing a hash tree.
+     *
+     * The generated signature block is wrapped into an APK Signing Block and inserted into the
+     * original APK immediately before the start of ZIP Central Directory. This is to ensure that
+     * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
+     * extensibility. For example, a future signature scheme could insert its signatures there as
+     * well. The contract of the APK Signing Block is that all contents outside of the block must be
+     * protected by signatures inside the block.
+     */
+
+    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+            V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SchemeSigner() {}
+
+    /**
+     * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
+     * provided key.
+     *
+     * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+     *     AndroidManifest.xml minSdkVersion attribute).
+     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+     *     Signature Scheme v2
+     */
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
+            throws InvalidKeyException {
+        String keyAlgorithm = signingKey.getAlgorithm();
+        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+            // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
+            // deterministic signatures which make life easier for OTA updates (fewer files
+            // changed when deterministic signature schemes are used).
+
+            // Pick a digest which is no weaker than the key.
+            int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
+            if (modulusLengthBits <= 3072) {
+                // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
+                List<SignatureAlgorithm> algorithms = new ArrayList<>();
+                algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+                if (verityEnabled) {
+                    algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
+                }
+                return algorithms;
+            } 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.
+                return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
+            }
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            // DSA is supported only with SHA-256.
+            List<SignatureAlgorithm> algorithms = new ArrayList<>();
+            algorithms.add(
+                    deterministicDsaSigning ?
+                            SignatureAlgorithm.DETDSA_WITH_SHA256 :
+                            SignatureAlgorithm.DSA_WITH_SHA256);
+            if (verityEnabled) {
+                algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
+            }
+            return algorithms;
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            // Pick a digest which is no weaker than the key.
+            int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
+            if (keySizeBits <= 256) {
+                // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
+                List<SignatureAlgorithm> algorithms = new ArrayList<>();
+                algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
+                if (verityEnabled) {
+                    algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
+                }
+                return algorithms;
+            } else {
+                // Keys longer than 256 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.
+                return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
+            }
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+            generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
+                DataSource beforeCentralDir,
+                DataSource centralDir,
+                DataSource eocd,
+                List<SignerConfig> signerConfigs,
+                boolean v3SigningEnabled)
+                throws IOException, InvalidKeyException, NoSuchAlgorithmException,
+                SignatureException {
+        return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
+                signerConfigs, v3SigningEnabled, null);
+    }
+
+    public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+            generateApkSignatureSchemeV2Block(
+                    RunnablesExecutor executor,
+                    DataSource beforeCentralDir,
+                    DataSource centralDir,
+                    DataSource eocd,
+                    List<SignerConfig> signerConfigs,
+                    boolean v3SigningEnabled,
+                    List<byte[]> preservedV2SignerBlocks)
+                    throws IOException, InvalidKeyException, NoSuchAlgorithmException,
+                            SignatureException {
+        Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+                ApkSigningBlockUtils.computeContentDigests(
+                        executor, beforeCentralDir, centralDir, eocd, signerConfigs);
+        return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
+                generateApkSignatureSchemeV2Block(
+                        digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
+                        preservedV2SignerBlocks),
+                digestInfo.getSecond());
+    }
+
+    private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
+            List<SignerConfig> signerConfigs,
+            Map<ContentDigestAlgorithm, byte[]> contentDigests,
+            boolean v3SigningEnabled,
+            List<byte[]> preservedV2SignerBlocks)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed signer blocks.
+
+        if (signerConfigs.size() > MAX_APK_SIGNERS) {
+            throw new IllegalArgumentException(
+                    "APK Signature Scheme v2 only supports a maximum of " + MAX_APK_SIGNERS + ", "
+                            + signerConfigs.size() + " provided");
+        }
+
+        List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+        if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
+            signerBlocks.addAll(preservedV2SignerBlocks);
+        }
+        int signerNumber = 0;
+        for (SignerConfig signerConfig : signerConfigs) {
+            signerNumber++;
+            byte[] signerBlock;
+            try {
+                signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
+            } catch (SignatureException e) {
+                throw new SignatureException("Signer #" + signerNumber + " failed", e);
+            }
+            signerBlocks.add(signerBlock);
+        }
+
+        return Pair.of(
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+                        }),
+                V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+    }
+
+    private static byte[] generateSignerBlock(
+            SignerConfig signerConfig,
+            Map<ContentDigestAlgorithm, byte[]> contentDigests,
+            boolean v3SigningEnabled)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        if (signerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+        PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+        byte[] encodedPublicKey = encodePublicKey(publicKey);
+
+        V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
+        try {
+            signedData.certificates = encodeCertificates(signerConfig.certificates);
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException("Failed to encode certificates", e);
+        }
+
+        List<Pair<Integer, byte[]>> digests =
+                new ArrayList<>(signerConfig.signatureAlgorithms.size());
+        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+            ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
+            if (contentDigest == null) {
+                throw new RuntimeException(
+                        contentDigestAlgorithm
+                                + " content digest for "
+                                + signatureAlgorithm
+                                + " not computed");
+            }
+            digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
+        }
+        signedData.digests = digests;
+        signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled);
+
+        V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed digests:
+        //   * uint32: signature algorithm ID
+        //   * length-prefixed bytes: digest of contents
+        // * length-prefixed sequence of certificates:
+        //   * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+        // * length-prefixed sequence of length-prefixed additional attributes:
+        //   * uint32: ID
+        //   * (length - 4) bytes: value
+
+        signer.signedData =
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                    signedData.digests),
+                            encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
+                            signedData.additionalAttributes,
+                            new byte[0],
+                        });
+        signer.publicKey = encodedPublicKey;
+        signer.signatures = new ArrayList<>();
+        signer.signatures =
+                ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
+
+        // FORMAT:
+        // * length-prefixed signed data
+        // * length-prefixed sequence of length-prefixed signatures:
+        //   * uint32: signature algorithm ID
+        //   * length-prefixed bytes: signature of signed data
+        // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
+        return encodeAsSequenceOfLengthPrefixedElements(
+                new byte[][] {
+                    signer.signedData,
+                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                            signer.signatures),
+                    signer.publicKey,
+                });
+    }
+
+    private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
+        if (v3SigningEnabled) {
+            // FORMAT (little endian):
+            // * length-prefixed bytes: attribute pair
+            //   * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
+            //   * uint32: value - 3 (v3 signature scheme id) in this case
+            int payloadSize = 4 + 4 + 4;
+            ByteBuffer result = ByteBuffer.allocate(payloadSize);
+            result.order(ByteOrder.LITTLE_ENDIAN);
+            result.putInt(payloadSize - 4);
+            result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
+            result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+            return result.array();
+        } else {
+            return new byte[0];
+        }
+    }
+
+    private static final class V2SignatureSchemeBlock {
+        private static final class Signer {
+            public byte[] signedData;
+            public List<Pair<Integer, byte[]>> signatures;
+            public byte[] publicKey;
+        }
+
+        private static final class SignedData {
+            public List<Pair<Integer, byte[]>> digests;
+            public List<byte[]> certificates;
+            public byte[] additionalAttributes;
+        }
+    }
+}

+ 471 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java

@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2016 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.apk.v2;
+
+import static com.android.apksig.Constants.MAX_APK_SIGNERS;
+
+import com.android.apksig.ApkVerifier.Issue;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+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.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;
+import com.android.apksig.util.RunnablesExecutor;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+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.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme v2 verifier.
+ *
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ */
+public abstract class V2SchemeVerifier {
+    /** Hidden constructor to prevent instantiation. */
+    private V2SchemeVerifier() {}
+
+    /**
+     * Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
+     * verification. The APK must be considered verified only if
+     * {@link ApkSigningBlockUtils.Result#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see
+     * {@link ApkSigningBlockUtils.Result#getErrors()}.
+     *
+     * <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to
+     * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
+     * If the APK's signature is expected to not verify on any of the specified platform versions,
+     * this method returns a result with one or more errors and whose
+     * {@code Result.verified == false}, or this method throws an exception.
+     *
+     * @throws ApkFormatException if the APK is malformed
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *         required cryptographic algorithm implementation is missing
+     * @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2
+     * signatures are found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigningBlockUtils.Result verify(
+            RunnablesExecutor executor,
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            Map<Integer, String> supportedApkSigSchemeNames,
+            Set<Integer> foundSigSchemeIds,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException,
+            ApkSigningBlockUtils.SignatureNotFoundException {
+        ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(apk, zipSections,
+                        V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
+
+        DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
+        DataSource centralDir =
+                apk.slice(
+                        signatureInfo.centralDirOffset,
+                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+        ByteBuffer eocd = signatureInfo.eocd;
+
+        verify(executor,
+                beforeApkSigningBlock,
+                signatureInfo.signatureBlock,
+                centralDir,
+                eocd,
+                supportedApkSigSchemeNames,
+                foundSigSchemeIds,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's v2 signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the
+     * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
+     * Set, int, int)} for more information about the contract of this method.
+     *
+     * @param result result populated by this method with interesting information about the APK,
+     *        such as information about signers, and verification errors and warnings.
+     */
+    private static void verify(
+            RunnablesExecutor executor,
+            DataSource beforeApkSigningBlock,
+            ByteBuffer apkSignatureSchemeV2Block,
+            DataSource centralDir,
+            ByteBuffer eocd,
+            Map<Integer, String> supportedApkSigSchemeNames,
+            Set<Integer> foundSigSchemeIds,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigningBlockUtils.Result result)
+            throws IOException, NoSuchAlgorithmException {
+        Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        parseSigners(
+                apkSignatureSchemeV2Block,
+                contentDigestsToVerify,
+                supportedApkSigSchemeNames,
+                foundSigSchemeIds,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        if (result.containsErrors()) {
+            return;
+        }
+        ApkSigningBlockUtils.verifyIntegrity(
+                executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+        if (!result.containsErrors()) {
+            result.verified = true;
+        }
+    }
+
+    /**
+     * Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding
+     * {@code signerInfos} of the provided {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+     * However, this does not verify the integrity of the rest of the APK but rather simply reports
+     * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the
+     * {@code [minSdkVersion, maxSdkVersion]} range.
+     */
+    public static void parseSigners(
+            ByteBuffer apkSignatureSchemeV2Block,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify,
+            Map<Integer, String> supportedApkSigSchemeNames,
+            Set<Integer> foundApkSigSchemeIds,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+        ByteBuffer signers;
+        try {
+            signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
+        } catch (ApkFormatException e) {
+            result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
+            return;
+        }
+        if (!signers.hasRemaining()) {
+            result.addError(Issue.V2_SIG_NO_SIGNERS);
+            return;
+        }
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+        }
+        int signerCount = 0;
+        while (signers.hasRemaining()) {
+            int signerIndex = signerCount;
+            signerCount++;
+            ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+                    new ApkSigningBlockUtils.Result.SignerInfo();
+            signerInfo.index = signerIndex;
+            result.signers.add(signerInfo);
+            try {
+                ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
+                parseSigner(
+                        signer,
+                        certFactory,
+                        signerInfo,
+                        contentDigestsToVerify,
+                        supportedApkSigSchemeNames,
+                        foundApkSigSchemeIds,
+                        minSdkVersion,
+                        maxSdkVersion);
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
+                return;
+            }
+        }
+        if (signerCount > MAX_APK_SIGNERS) {
+            result.addError(Issue.V2_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, signerCount);
+        }
+    }
+
+    /**
+     * Parses the provided signer block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+     * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+     * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+     * integrity of the APK.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the
+     * {@code [minSdkVersion, maxSdkVersion]} range.
+     */
+    private static void parseSigner(
+            ByteBuffer signerBlock,
+            CertificateFactory certFactory,
+            ApkSigningBlockUtils.Result.SignerInfo result,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify,
+            Map<Integer, String> supportedApkSigSchemeNames,
+            Set<Integer> foundApkSigSchemeIds,
+            int minSdkVersion,
+            int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
+        ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
+        byte[] signedDataBytes = new byte[signedData.remaining()];
+        signedData.get(signedDataBytes);
+        signedData.flip();
+        result.signedData = signedDataBytes;
+
+        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
+        byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
+
+        // Parse the signatures block and identify supported signatures
+        int signatureCount = 0;
+        List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+        while (signatures.hasRemaining()) {
+            signatureCount++;
+            try {
+                ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+                int sigAlgorithmId = signature.getInt();
+                byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+                result.signatures.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.Signature(
+                                sigAlgorithmId, sigBytes));
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+                    continue;
+                }
+                supportedSignatures.add(
+                        new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
+                return;
+            }
+        }
+        if (result.signatures.isEmpty()) {
+            result.addError(Issue.V2_SIG_NO_SIGNATURES);
+            return;
+        }
+
+        // Verify signatures over signed-data block using the public key
+        List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
+        try {
+            signaturesToVerify =
+                    ApkSigningBlockUtils.getSignaturesToVerify(
+                            supportedSignatures, minSdkVersion, maxSdkVersion);
+        } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+            result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
+            return;
+        }
+        for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+            String jcaSignatureAlgorithm =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+            String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+            PublicKey publicKey;
+            try {
+                publicKey =
+                        KeyFactory.getInstance(keyAlgorithm).generatePublic(
+                                new X509EncodedKeySpec(publicKeyBytes));
+            } catch (Exception e) {
+                result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
+                return;
+            }
+            try {
+                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                sig.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    sig.setParameter(jcaSignatureAlgorithmParams);
+                }
+                signedData.position(0);
+                sig.update(signedData);
+                byte[] sigBytes = signature.signature;
+                if (!sig.verify(sigBytes)) {
+                    result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+                    return;
+                }
+                result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+                contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+            } catch (InvalidKeyException | InvalidAlgorithmParameterException
+                    | SignatureException e) {
+                result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                return;
+            }
+        }
+
+        // At least one signature over signedData has verified. We can now parse signed-data.
+        signedData.position(0);
+        ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+        ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+        ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+
+        // Parse the certificates block
+        int certificateIndex = -1;
+        while (certificates.hasRemaining()) {
+            certificateIndex++;
+            byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
+            X509Certificate certificate;
+            try {
+                certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
+            } catch (CertificateException e) {
+                result.addError(
+                        Issue.V2_SIG_MALFORMED_CERTIFICATE,
+                        certificateIndex,
+                        certificateIndex + 1,
+                        e);
+                return;
+            }
+            // 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(certificate, encodedCert);
+            result.certs.add(certificate);
+        }
+
+        if (result.certs.isEmpty()) {
+            result.addError(Issue.V2_SIG_NO_CERTIFICATES);
+            return;
+        }
+        X509Certificate mainCertificate = result.certs.get(0);
+        byte[] certificatePublicKeyBytes;
+        try {
+            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+                    mainCertificate.getPublicKey());
+        } catch (InvalidKeyException e) {
+            System.out.println("Caught an exception encoding the public key: " + e);
+            e.printStackTrace();
+            certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+        }
+        if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+            result.addError(
+                    Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+                    ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+                    ApkSigningBlockUtils.toHex(publicKeyBytes));
+            return;
+        }
+
+        // Parse the digests block
+        int digestCount = 0;
+        while (digests.hasRemaining()) {
+            digestCount++;
+            try {
+                ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
+                int sigAlgorithmId = digest.getInt();
+                byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
+                result.contentDigests.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+                                sigAlgorithmId, digestBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
+                return;
+            }
+        }
+
+        List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
+        for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
+            sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
+        }
+        List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
+            sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
+        }
+
+        if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
+            result.addError(
+                    Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
+                    sigAlgsFromSignaturesRecord,
+                    sigAlgsFromDigestsRecord);
+            return;
+        }
+
+        // Parse the additional attributes block.
+        int additionalAttributeCount = 0;
+        Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
+        Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
+        while (additionalAttributes.hasRemaining()) {
+            additionalAttributeCount++;
+            try {
+                ByteBuffer attribute =
+                        ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes);
+                int id = attribute.getInt();
+                byte[] value = ByteBufferUtils.toByteArray(attribute);
+                result.additionalAttributes.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
+                switch (id) {
+                    case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
+                        // stripping protection added when signing with a newer scheme
+                        int foundId = ByteBuffer.wrap(value).order(
+                                ByteOrder.LITTLE_ENDIAN).getInt();
+                        if (supportedApkSigSchemeIds.contains(foundId)) {
+                            supportedExpectedApkSigSchemeIds.add(foundId);
+                        } else {
+                            result.addWarning(
+                                    Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
+                        }
+                        break;
+                    default:
+                        result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
+                }
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addError(
+                        Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
+                return;
+            }
+        }
+
+        // make sure that all known IDs indicated in stripping protection have already verified
+        for (int id : supportedExpectedApkSigSchemeIds) {
+            if (!foundApkSigSchemeIds.contains(id)) {
+                String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
+                result.addError(
+                        Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
+                        result.index,
+                        apkSigSchemeName);
+            }
+        }
+    }
+}

+ 66 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 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.apk.v3;
+
+import com.android.apksig.internal.util.AndroidSdkVersion;
+
+/** Constants used by the V3 Signature Scheme signing and verification. */
+public class V3SchemeConstants {
+    private V3SchemeConstants() {}
+
+    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+    public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
+    public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+    public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
+    public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
+    /**
+     * By default, APK signing key rotation will target T, but packages that have previously
+     * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the
+     * --rotation-min-sdk-version parameter when using apksigner or when invoking
+     * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
+     */
+    public static final int DEFAULT_ROTATION_MIN_SDK_VERSION  = AndroidSdkVersion.T;
+
+    /**
+     * This attribute is intended to be written to the V3.0 signer block as an additional attribute
+     * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
+     * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
+     * for rotation in the v3.1 signing block is not X, then the APK should be rejected.
+     */
+    public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
+
+    /**
+     * This attribute is written to the V3.1 signer block as an additional attribute to signify that
+     * the rotation-min-sdk-version is targeting a development release. This is required to support
+     * testing rotation on new development releases as the previous platform release SDK version
+     * is used as the development release SDK version until the development release SDK is
+     * finalized.
+     */
+    public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
+
+    /**
+     * The current development release; rotation / signing configs targeting this release should
+     * be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute.
+     */
+    public static final int DEV_RELEASE = AndroidSdkVersion.U;
+
+    /**
+     * The current production release.
+     */
+    public static final int PROD_RELEASE = AndroidSdkVersion.T;
+}

+ 531 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java

@@ -0,0 +1,531 @@
+/*
+ * 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.apk.v3;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
+
+import com.android.apksig.SigningCertificateLineage;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalInt;
+
+/**
+ * APK Signature Scheme v3 signer.
+ *
+ * <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
+ * Signature Scheme v2 goals.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ *     <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
+ *     SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
+ *     it can prove the new siging certificate was signed by the old.
+ */
+public class V3SchemeSigner {
+    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+    public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+    private final RunnablesExecutor mExecutor;
+    private final DataSource mBeforeCentralDir;
+    private final DataSource mCentralDir;
+    private final DataSource mEocd;
+    private final List<SignerConfig> mSignerConfigs;
+    private final int mBlockId;
+    private final OptionalInt mOptionalV31MinSdkVersion;
+    private final boolean mRotationTargetsDevRelease;
+
+    private V3SchemeSigner(DataSource beforeCentralDir,
+            DataSource centralDir,
+            DataSource eocd,
+            List<SignerConfig> signerConfigs,
+            RunnablesExecutor executor,
+            int blockId,
+            OptionalInt optionalV31MinSdkVersion,
+            boolean rotationTargetsDevRelease) {
+        mBeforeCentralDir = beforeCentralDir;
+        mCentralDir = centralDir;
+        mEocd = eocd;
+        mSignerConfigs = signerConfigs;
+        mExecutor = executor;
+        mBlockId = blockId;
+        mOptionalV31MinSdkVersion = optionalV31MinSdkVersion;
+        mRotationTargetsDevRelease = rotationTargetsDevRelease;
+    }
+
+    /**
+     * Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
+     * provided key.
+     *
+     * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+     *     AndroidManifest.xml minSdkVersion attribute).
+     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+     *     Signature Scheme v3
+     */
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
+            throws InvalidKeyException {
+        String keyAlgorithm = signingKey.getAlgorithm();
+        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+            // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
+            // deterministic signatures which make life easier for OTA updates (fewer files
+            // changed when deterministic signature schemes are used).
+
+            // Pick a digest which is no weaker than the key.
+            int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
+            if (modulusLengthBits <= 3072) {
+                // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
+                List<SignatureAlgorithm> algorithms = new ArrayList<>();
+                algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+                if (verityEnabled) {
+                    algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
+                }
+                return algorithms;
+            } 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.
+                return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
+            }
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            // DSA is supported only with SHA-256.
+            List<SignatureAlgorithm> algorithms = new ArrayList<>();
+            algorithms.add(
+                    deterministicDsaSigning ?
+                            SignatureAlgorithm.DETDSA_WITH_SHA256 :
+                            SignatureAlgorithm.DSA_WITH_SHA256);
+            if (verityEnabled) {
+                algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
+            }
+            return algorithms;
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            // Pick a digest which is no weaker than the key.
+            int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
+            if (keySizeBits <= 256) {
+                // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
+                List<SignatureAlgorithm> algorithms = new ArrayList<>();
+                algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
+                if (verityEnabled) {
+                    algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
+                }
+                return algorithms;
+            } else {
+                // Keys longer than 256 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.
+                return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
+            }
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
+            RunnablesExecutor executor,
+            DataSource beforeCentralDir,
+            DataSource centralDir,
+            DataSource eocd,
+            List<SignerConfig> signerConfigs)
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+        return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
+                .setRunnablesExecutor(executor)
+                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+                .build()
+                .generateApkSignatureSchemeV3BlockAndDigests();
+    }
+
+    public static byte[] generateV3SignerAttribute(
+            SigningCertificateLineage signingCertificateLineage) {
+        // FORMAT (little endian):
+        // * length-prefixed bytes: attribute pair
+        //   * uint32: ID
+        //   * bytes: value - encoded V3 SigningCertificateLineage
+        byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
+        int payloadSize = 4 + 4 + encodedLineage.length;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(4 + encodedLineage.length);
+        result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
+        result.put(encodedLineage);
+        return result.array();
+    }
+
+    private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+            int rotationMinSdkVersion) {
+        // FORMAT (little endian):
+        // * length-prefixed bytes: attribute pair
+        //   * uint32: ID
+        //   * bytes: value - int32 representing minimum SDK version for rotation
+        int payloadSize = 4 + 4 + 4;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(payloadSize - 4);
+        result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
+        result.putInt(rotationMinSdkVersion);
+        return result.array();
+    }
+
+    private static byte[] generateV31RotationTargetsDevReleaseAttribute() {
+        // FORMAT (little endian):
+        // * length-prefixed bytes: attribute pair
+        //   * uint32: ID
+        //   * bytes: value - No value is used for this attribute
+        int payloadSize = 4 + 4;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(payloadSize - 4);
+        result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+        return result.array();
+    }
+
+    /**
+     * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
+     * signing scheme block and digests based on the parameters provided to the {@link Builder}.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+     *         missing
+     * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
+     * @throws SignatureException if an error occurs when computing digests or generating
+     *         signatures
+     */
+    public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+        Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+                ApkSigningBlockUtils.computeContentDigests(
+                        mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
+        return new SigningSchemeBlockAndDigests(
+                generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
+    }
+
+    private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
+            Map<ContentDigestAlgorithm, byte[]> contentDigests)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed signer blocks.
+        List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
+        int signerNumber = 0;
+        for (SignerConfig signerConfig : mSignerConfigs) {
+            signerNumber++;
+            byte[] signerBlock;
+            try {
+                signerBlock = generateSignerBlock(signerConfig, contentDigests);
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
+            } catch (SignatureException e) {
+                throw new SignatureException("Signer #" + signerNumber + " failed", e);
+            }
+            signerBlocks.add(signerBlock);
+        }
+
+        return Pair.of(
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+                        }),
+                mBlockId);
+    }
+
+    private byte[] generateSignerBlock(
+            SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        if (signerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+        PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+        byte[] encodedPublicKey = encodePublicKey(publicKey);
+
+        V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
+        try {
+            signedData.certificates = encodeCertificates(signerConfig.certificates);
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException("Failed to encode certificates", e);
+        }
+
+        List<Pair<Integer, byte[]>> digests =
+                new ArrayList<>(signerConfig.signatureAlgorithms.size());
+        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+            ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
+            if (contentDigest == null) {
+                throw new RuntimeException(
+                        contentDigestAlgorithm
+                                + " content digest for "
+                                + signatureAlgorithm
+                                + " not computed");
+            }
+            digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
+        }
+        signedData.digests = digests;
+        signedData.minSdkVersion = signerConfig.minSdkVersion;
+        signedData.maxSdkVersion = signerConfig.maxSdkVersion;
+        signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
+
+        V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
+
+        signer.signedData = encodeSignedData(signedData);
+
+        signer.minSdkVersion = signerConfig.minSdkVersion;
+        signer.maxSdkVersion = signerConfig.maxSdkVersion;
+        signer.publicKey = encodedPublicKey;
+        signer.signatures =
+                ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
+
+        return encodeSigner(signer);
+    }
+
+    private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
+        byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
+        byte[] signatures =
+                encodeAsLengthPrefixedElement(
+                        encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                signer.signatures));
+        byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey);
+
+        // FORMAT:
+        // * length-prefixed signed data
+        // * uint32: minSdkVersion
+        // * uint32: maxSdkVersion
+        // * length-prefixed sequence of length-prefixed signatures:
+        //   * uint32: signature algorithm ID
+        //   * length-prefixed bytes: signature of signed data
+        // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
+        int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
+
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.put(signedData);
+        result.putInt(signer.minSdkVersion);
+        result.putInt(signer.maxSdkVersion);
+        result.put(signatures);
+        result.put(publicKey);
+
+        return result.array();
+    }
+
+    private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
+        byte[] digests =
+                encodeAsLengthPrefixedElement(
+                        encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                signedData.digests));
+        byte[] certs =
+                encodeAsLengthPrefixedElement(
+                        encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
+        byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed digests:
+        //   * uint32: signature algorithm ID
+        //   * length-prefixed bytes: digest of contents
+        // * length-prefixed sequence of certificates:
+        //   * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+        // * uint-32: minSdkVersion
+        // * uint-32: maxSdkVersion
+        // * length-prefixed sequence of length-prefixed additional attributes:
+        //   * uint32: ID
+        //   * (length - 4) bytes: value
+        //   * uint32: Proof-of-rotation ID: 0x3ba06f8c
+        //   * length-prefixed roof-of-rotation structure
+        int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
+
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.put(digests);
+        result.put(certs);
+        result.putInt(signedData.minSdkVersion);
+        result.putInt(signedData.maxSdkVersion);
+        result.put(attributes);
+
+        return result.array();
+    }
+
+    private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
+        List<byte[]> attributes = new ArrayList<>();
+        if (signerConfig.signingCertificateLineage != null) {
+            attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage));
+        }
+        if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease)
+                && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+            attributes.add(generateV31RotationTargetsDevReleaseAttribute());
+        }
+        if (mOptionalV31MinSdkVersion.isPresent()
+                && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+            attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+                    mOptionalV31MinSdkVersion.getAsInt()));
+        }
+        int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum();
+        byte[] attributesBuffer = new byte[attributesSize];
+        if (attributesSize == 0) {
+            return new byte[0];
+        }
+        int index = 0;
+        for (byte[] attribute : attributes) {
+            System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length);
+            index += attribute.length;
+        }
+        return attributesBuffer;
+    }
+
+    private static final class V3SignatureSchemeBlock {
+        private static final class Signer {
+            public byte[] signedData;
+            public int minSdkVersion;
+            public int maxSdkVersion;
+            public List<Pair<Integer, byte[]>> signatures;
+            public byte[] publicKey;
+        }
+
+        private static final class SignedData {
+            public List<Pair<Integer, byte[]>> digests;
+            public List<byte[]> certificates;
+            public int minSdkVersion;
+            public int maxSdkVersion;
+            public byte[] additionalAttributes;
+        }
+    }
+
+    /** Builder of {@link V3SchemeSigner} instances. */
+    public static class Builder {
+        private final DataSource mBeforeCentralDir;
+        private final DataSource mCentralDir;
+        private final DataSource mEocd;
+        private final List<SignerConfig> mSignerConfigs;
+
+        private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+        private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+        private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty();
+        private boolean mRotationTargetsDevRelease = false;
+
+        /**
+         * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
+         * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
+         * be used to sign the APK.
+         */
+        public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
+                List<SignerConfig> signerConfigs) {
+            mBeforeCentralDir = beforeCentralDir;
+            mCentralDir = centralDir;
+            mEocd = eocd;
+            mSignerConfigs = signerConfigs;
+        }
+
+        /**
+         * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
+         */
+        public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+            mExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Sets the {@code blockId} to be used for the V3 signature block.
+         *
+         * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+         */
+        public Builder setBlockId(int blockId) {
+            mBlockId = blockId;
+            return this;
+        }
+
+        /**
+         * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
+         * signer's block.
+         *
+         * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
+         * is not modified or removed from the APK's signature block.
+         */
+        public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+            return setMinSdkVersionForV31(rotationMinSdkVersion);
+        }
+
+        /**
+         * Sets the {@code minSdkVersion} to be written as an additional attribute in each
+         * signer's block.
+         *
+         * <p>This value provides the stripping protection to ensure a v3.1 signing block is not
+         * modified or removed from the APK's signature block.
+         */
+        public Builder setMinSdkVersionForV31(int minSdkVersion) {
+            if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+                minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+            }
+            mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion);
+            return this;
+        }
+
+        /**
+         * Sets whether the minimum SDK version of a signer is intended to target a development
+         * release; this is primarily required after the T SDK is finalized, and an APK needs to
+         * target U during its development cycle for rotation.
+         *
+         * <p>This is only required after the T SDK is finalized since S and earlier releases do
+         * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+         * use the SDK version of T during development. A signer with a minimum SDK version of T's
+         * SDK version along with setting {@code enabled} to true will allow an APK to use the
+         * rotated key on a device running U while causing this to be bypassed for T.
+         *
+         * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+         * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+         * will be a noop.
+         */
+        public Builder setRotationTargetsDevRelease(boolean enabled) {
+            mRotationTargetsDevRelease = enabled;
+            return this;
+        }
+
+        /**
+         * Returns a new {@link V3SchemeSigner} built with the configuration provided to this
+         * {@code Builder}.
+         */
+        public V3SchemeSigner build() {
+            return new V3SchemeSigner(mBeforeCentralDir,
+                    mCentralDir,
+                    mEocd,
+                    mSignerConfigs,
+                    mExecutor,
+                    mBlockId,
+                    mOptionalV31MinSdkVersion,
+                    mRotationTargetsDevRelease);
+        }
+    }
+}

+ 783 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java

@@ -0,0 +1,783 @@
+/*
+ * 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.apk.v3;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.ApkVerifier.Issue;
+import com.android.apksig.SigningCertificateLineage;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException;
+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.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+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.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * APK Signature Scheme v3 verifier.
+ *
+ * <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
+ * single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ */
+public class V3SchemeVerifier {
+    private final RunnablesExecutor mExecutor;
+    private final DataSource mApk;
+    private final ApkUtils.ZipSections mZipSections;
+    private final ApkSigningBlockUtils.Result mResult;
+    private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+    private final int mMinSdkVersion;
+    private final int mMaxSdkVersion;
+    private final int mBlockId;
+    private final OptionalInt mOptionalRotationMinSdkVersion;
+    private final boolean mFullVerification;
+
+    private ByteBuffer mApkSignatureSchemeV3Block;
+
+    private V3SchemeVerifier(
+            RunnablesExecutor executor,
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify,
+            ApkSigningBlockUtils.Result result,
+            int minSdkVersion,
+            int maxSdkVersion,
+            int blockId,
+            OptionalInt optionalRotationMinSdkVersion,
+            boolean fullVerification) {
+        mExecutor = executor;
+        mApk = apk;
+        mZipSections = zipSections;
+        mContentDigestsToVerify = contentDigestsToVerify;
+        mResult = result;
+        mMinSdkVersion = minSdkVersion;
+        mMaxSdkVersion = maxSdkVersion;
+        mBlockId = blockId;
+        mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+        mFullVerification = fullVerification;
+    }
+
+    /**
+     * Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
+     * verification. The APK must be considered verified only if
+     * {@link ApkSigningBlockUtils.Result#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see
+     * {@link ApkSigningBlockUtils.Result#getErrors()}.
+     *
+     * <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
+     * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
+     * If the APK's signature is expected to not verify on any of the specified platform versions,
+     * this method returns a result with one or more errors and whose
+     * {@code Result.verified == false}, or this method throws an exception.
+     *
+     * <p>This method only verifies the v3.0 signing block without platform targeted rotation from
+     * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
+     * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
+     *
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *         required cryptographic algorithm implementation is missing
+     * @throws SignatureNotFoundException if no APK Signature Scheme v3
+     * signatures are found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigningBlockUtils.Result verify(
+            RunnablesExecutor executor,
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+        return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
+                .setRunnablesExecutor(executor)
+                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+                .build()
+                .verify();
+    }
+
+    /**
+     * Verifies the provided APK's v3 signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the
+     * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
+     * int)} for more information about the contract of this method.
+     *
+     * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
+     *        APK, such as information about signers, and verification errors and warnings
+     */
+    public ApkSigningBlockUtils.Result verify()
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+        if (mApk == null || mZipSections == null) {
+            throw new IllegalStateException(
+                    "A non-null apk and zip sections must be specified to verify an APK's v3 "
+                            + "signatures");
+        }
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+        mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+
+        DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
+        DataSource centralDir =
+                mApk.slice(
+                        signatureInfo.centralDirOffset,
+                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+        ByteBuffer eocd = signatureInfo.eocd;
+
+        parseSigners();
+
+        if (mResult.containsErrors()) {
+            return mResult;
+        }
+        ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
+                mContentDigestsToVerify, mResult);
+
+        // make sure that the v3 signers cover the entire targeted sdk version ranges and that the
+        // longest SigningCertificateHistory, if present, corresponds to the newest platform
+        // versions
+        SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
+        for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
+            sortedSigners.put(signer.maxSdkVersion, signer);
+        }
+
+        // first make sure there is neither overlap nor holes
+        int firstMin = 0;
+        int lastMax = 0;
+        int lastLineageSize = 0;
+
+        // while we're iterating through the signers, build up the list of lineages
+        List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());
+
+        for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
+            int currentMin = signer.minSdkVersion;
+            int currentMax = signer.maxSdkVersion;
+            if (firstMin == 0) {
+                // first round sets up our basis
+                firstMin = currentMin;
+            } else {
+                // A signer's minimum SDK can equal the previous signer's maximum SDK if this signer
+                // is targeting a development release.
+                if (currentMin != (lastMax + 1)
+                        && !(currentMin == lastMax && signerTargetsDevRelease(signer))) {
+                    mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
+                    break;
+                }
+            }
+            lastMax = currentMax;
+
+            // also, while we're here, make sure that the lineage sizes only increase
+            if (signer.signingCertificateLineage != null) {
+                int currLineageSize = signer.signingCertificateLineage.size();
+                if (currLineageSize < lastLineageSize) {
+                    mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
+                    break;
+                }
+                lastLineageSize = currLineageSize;
+                lineages.add(signer.signingCertificateLineage);
+            }
+        }
+
+        // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
+        // then the max level only needs to support up to that sdk version for rotation.
+        if (firstMin > mMinSdkVersion
+                || lastMax < (mOptionalRotationMinSdkVersion.isPresent()
+                    ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
+            mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
+        }
+
+        try {
+            mResult.signingCertificateLineage =
+                    SigningCertificateLineage.consolidateLineages(lineages);
+        } catch (IllegalArgumentException e) {
+            mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
+        }
+        if (!mResult.containsErrors()) {
+            mResult.verified = true;
+        }
+        return mResult;
+    }
+
+    /**
+     * Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
+     * {@code signerInfos} of the provided {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+     * However, this does not verify the integrity of the rest of the APK but rather simply reports
+     * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the
+     * {@code [minSdkVersion, maxSdkVersion]} range.
+     */
+    public static void parseSigners(
+            ByteBuffer apkSignatureSchemeV3Block,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify,
+            ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+        try {
+            new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
+                    .setResult(result)
+                    .setContentDigestsToVerify(contentDigestsToVerify)
+                    .setFullVerification(false)
+                    .build()
+                    .parseSigners();
+        } catch (IOException | SignatureNotFoundException e) {
+            // This should never occur since the apkSignatureSchemeV3Block was already provided.
+            throw new IllegalStateException("An exception was encountered when attempting to parse"
+                    + " the signers from the provided APK Signature Scheme v3 block", e);
+        }
+    }
+
+    /**
+     * Parses each signer in the APK Signature Scheme v3 block and populates corresponding
+     * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
+     * returned {@link ApkSigningBlockUtils.Result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+     * However, this does not verify the integrity of the rest of the APK but rather simply reports
+     * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
+     *
+     * <p>This method adds one or more errors to the returned {@code Result} if a verification error
+     * is encountered when parsing the signers.
+     */
+    public ApkSigningBlockUtils.Result parseSigners()
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+        ByteBuffer signers;
+        try {
+            if (mApkSignatureSchemeV3Block == null) {
+                SignatureInfo signatureInfo =
+                        ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+                mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+            }
+            signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
+        } catch (ApkFormatException e) {
+            mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
+            return mResult;
+        }
+        if (!signers.hasRemaining()) {
+            mResult.addError(Issue.V3_SIG_NO_SIGNERS);
+            return mResult;
+        }
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+        }
+        int signerCount = 0;
+        while (signers.hasRemaining()) {
+            int signerIndex = signerCount;
+            signerCount++;
+            ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+                    new ApkSigningBlockUtils.Result.SignerInfo();
+            signerInfo.index = signerIndex;
+            mResult.signers.add(signerInfo);
+            try {
+                ByteBuffer signer = getLengthPrefixedSlice(signers);
+                parseSigner(signer, certFactory, signerInfo);
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
+                return mResult;
+            }
+        }
+        return mResult;
+    }
+
+    /**
+     * Parses the provided signer block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} contained in this block, as well as
+     * the data contained therein, but does not verify the integrity of the rest of the APK. To
+     * facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
+     * These digests can then be used to verify the integrity of the APK.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the
+     * {@code [minSdkVersion, maxSdkVersion]} range.
+     */
+    private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
+            ApkSigningBlockUtils.Result.SignerInfo result)
+            throws ApkFormatException, NoSuchAlgorithmException {
+        ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+        byte[] signedDataBytes = new byte[signedData.remaining()];
+        signedData.get(signedDataBytes);
+        signedData.flip();
+        result.signedData = signedDataBytes;
+
+        int parsedMinSdkVersion = signerBlock.getInt();
+        int parsedMaxSdkVersion = signerBlock.getInt();
+        result.minSdkVersion = parsedMinSdkVersion;
+        result.maxSdkVersion = parsedMaxSdkVersion;
+        if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
+            result.addError(
+                    Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
+        }
+        ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+        byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+        // Parse the signatures block and identify supported signatures
+        int signatureCount = 0;
+        List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+        while (signatures.hasRemaining()) {
+            signatureCount++;
+            try {
+                ByteBuffer signature = getLengthPrefixedSlice(signatures);
+                int sigAlgorithmId = signature.getInt();
+                byte[] sigBytes = readLengthPrefixedByteArray(signature);
+                result.signatures.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.Signature(
+                                sigAlgorithmId, sigBytes));
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+                    continue;
+                }
+                // TODO consider dropping deprecated signatures for v3 or modifying
+                // getSignaturesToVerify (called below)
+                supportedSignatures.add(
+                        new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
+                return;
+            }
+        }
+        if (result.signatures.isEmpty()) {
+            result.addError(Issue.V3_SIG_NO_SIGNATURES);
+            return;
+        }
+
+        // Verify signatures over signed-data block using the public key
+        List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
+        try {
+            signaturesToVerify =
+                    ApkSigningBlockUtils.getSignaturesToVerify(
+                            supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
+        } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+            result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
+            return;
+        }
+        for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+            String jcaSignatureAlgorithm =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+            String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+            PublicKey publicKey;
+            try {
+                publicKey =
+                        KeyFactory.getInstance(keyAlgorithm).generatePublic(
+                                new X509EncodedKeySpec(publicKeyBytes));
+            } catch (Exception e) {
+                result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
+                return;
+            }
+            try {
+                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                sig.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    sig.setParameter(jcaSignatureAlgorithmParams);
+                }
+                signedData.position(0);
+                sig.update(signedData);
+                byte[] sigBytes = signature.signature;
+                if (!sig.verify(sigBytes)) {
+                    result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+                    return;
+                }
+                result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+                mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+            } catch (InvalidKeyException | InvalidAlgorithmParameterException
+                    | SignatureException e) {
+                result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                return;
+            }
+        }
+
+        // At least one signature over signedData has verified. We can now parse signed-data.
+        signedData.position(0);
+        ByteBuffer digests = getLengthPrefixedSlice(signedData);
+        ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+
+        int signedMinSdkVersion = signedData.getInt();
+        if (signedMinSdkVersion != parsedMinSdkVersion) {
+            result.addError(
+                    Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
+                    parsedMinSdkVersion,
+                    signedMinSdkVersion);
+        }
+        int signedMaxSdkVersion = signedData.getInt();
+        if (signedMaxSdkVersion != parsedMaxSdkVersion) {
+            result.addError(
+                    Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
+                    parsedMaxSdkVersion,
+                    signedMaxSdkVersion);
+        }
+        ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+
+        // Parse the certificates block
+        int certificateIndex = -1;
+        while (certificates.hasRemaining()) {
+            certificateIndex++;
+            byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+            X509Certificate certificate;
+            try {
+                certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
+            } catch (CertificateException e) {
+                result.addError(
+                        Issue.V3_SIG_MALFORMED_CERTIFICATE,
+                        certificateIndex,
+                        certificateIndex + 1,
+                        e);
+                return;
+            }
+            // 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(certificate, encodedCert);
+            result.certs.add(certificate);
+        }
+
+        if (result.certs.isEmpty()) {
+            result.addError(Issue.V3_SIG_NO_CERTIFICATES);
+            return;
+        }
+        X509Certificate mainCertificate = result.certs.get(0);
+        byte[] certificatePublicKeyBytes;
+        try {
+            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+                    mainCertificate.getPublicKey());
+        } catch (InvalidKeyException e) {
+            System.out.println("Caught an exception encoding the public key: " + e);
+            e.printStackTrace();
+            certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+        }
+        if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+            result.addError(
+                    Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+                    ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+                    ApkSigningBlockUtils.toHex(publicKeyBytes));
+            return;
+        }
+
+        // Parse the digests block
+        int digestCount = 0;
+        while (digests.hasRemaining()) {
+            digestCount++;
+            try {
+                ByteBuffer digest = getLengthPrefixedSlice(digests);
+                int sigAlgorithmId = digest.getInt();
+                byte[] digestBytes = readLengthPrefixedByteArray(digest);
+                result.contentDigests.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+                                sigAlgorithmId, digestBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
+                return;
+            }
+        }
+
+        List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
+        for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
+            sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
+        }
+        List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
+            sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
+        }
+
+        if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
+            result.addError(
+                    Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
+                    sigAlgsFromSignaturesRecord,
+                    sigAlgsFromDigestsRecord);
+            return;
+        }
+
+        // Parse the additional attributes block.
+        int additionalAttributeCount = 0;
+        boolean rotationAttrFound = false;
+        while (additionalAttributes.hasRemaining()) {
+            additionalAttributeCount++;
+            try {
+                ByteBuffer attribute =
+                        getLengthPrefixedSlice(additionalAttributes);
+                int id = attribute.getInt();
+                byte[] value = ByteBufferUtils.toByteArray(attribute);
+                result.additionalAttributes.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
+                if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
+                    try {
+                        // SigningCertificateLineage is verified when built
+                        result.signingCertificateLineage =
+                                SigningCertificateLineage.readFromV3AttributeValue(value);
+                        // make sure that the last cert in the chain matches this signer cert
+                        SigningCertificateLineage subLineage =
+                                result.signingCertificateLineage.getSubLineage(result.certs.get(0));
+                        if (result.signingCertificateLineage.size() != subLineage.size()) {
+                            result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
+                        }
+                    } catch (SecurityException e) {
+                        result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
+                    } catch (IllegalArgumentException e) {
+                        result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
+                    } catch (Exception e) {
+                        result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
+                    }
+                } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
+                    rotationAttrFound = true;
+                    // API targeting for rotation was added with V3.1; if the maxSdkVersion
+                    // does not support v3.1 then ignore this attribute.
+                    if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
+                            && mFullVerification) {
+                        int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
+                                .order(ByteOrder.LITTLE_ENDIAN).getInt();
+                        if (mOptionalRotationMinSdkVersion.isPresent()) {
+                            int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
+                            if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
+                                result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
+                                    attrRotationMinSdkVersion, rotationMinSdkVersion);
+                            }
+                        } else {
+                            result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
+                        }
+                    }
+                } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
+                    // This attribute should only be used by a v3.1 signer to indicate rotation
+                    // is targeting the development release that is using the SDK version of the
+                    // previously released platform version.
+                    if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+                        result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
+                    }
+                } else {
+                    result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
+                }
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addError(
+                        Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
+                return;
+            }
+        }
+        if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
+            result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
+                    mOptionalRotationMinSdkVersion.getAsInt());
+        }
+    }
+
+    /**
+     * Returns whether the specified {@code signerInfo} is targeting a development release.
+     */
+    public static boolean signerTargetsDevRelease(
+            ApkSigningBlockUtils.Result.SignerInfo signerInfo) {
+        boolean result = signerInfo.additionalAttributes.stream()
+                .mapToInt(attribute -> attribute.getId())
+                .anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+        return result;
+    }
+
+    /** Builder of {@link V3SchemeVerifier} instances. */
+    public static class Builder {
+        private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+        private DataSource mApk;
+        private ApkUtils.ZipSections mZipSections;
+        private ByteBuffer mApkSignatureSchemeV3Block;
+        private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+        private ApkSigningBlockUtils.Result mResult;
+        private int mMinSdkVersion;
+        private int mMaxSdkVersion;
+        private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+        private boolean mFullVerification = true;
+        private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+
+        /**
+         * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+         * verify the V3 signing block of the provided {@code apk} with the specified {@code
+         * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
+         */
+        public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
+                int maxSdkVersion) {
+            mApk = apk;
+            mZipSections = zipSections;
+            mMinSdkVersion = minSdkVersion;
+            mMaxSdkVersion = maxSdkVersion;
+        }
+
+        /**
+         * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+         * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
+         * apkSignatureSchemeV3Block}.
+         *
+         * <note>Full verification of the v3 signature is not possible when instantiating a new
+         * {@code V3SchemeVerifier} with this method.</note>
+         */
+        public Builder(ByteBuffer apkSignatureSchemeV3Block) {
+            mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
+        }
+
+        /**
+         * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
+         */
+        public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+            mExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Sets the V3 {code blockId} to be verified in the provided APK.
+         *
+         * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+         */
+        public Builder setBlockId(int blockId) {
+            mBlockId = blockId;
+            return this;
+        }
+
+        /**
+         * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
+         * attribute.
+         *
+         * <p>This value can be obtained from the signers returned when verifying the v3.1 signing
+         * block of an APK; in the case of multiple signers targeting different SDK versions in the
+         * v3.1 signing block, the minimum SDK version from all the signers should be used.
+         */
+        public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+            mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+            return this;
+        }
+
+        /**
+         * Sets the {@code result} instance to be used when returning verification results.
+         *
+         * <p>This method can be used when the caller already has a {@link
+         * ApkSigningBlockUtils.Result} and wants to store the verification results in this
+         * instance.
+         */
+        public Builder setResult(ApkSigningBlockUtils.Result result) {
+            mResult = result;
+            return this;
+        }
+
+        /**
+         * Sets the instance to be used to store the {@code contentDigestsToVerify}.
+         *
+         * <p>This method can be used when the caller needs access to the {@code
+         * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
+         */
+        public Builder setContentDigestsToVerify(
+                Set<ContentDigestAlgorithm> contentDigestsToVerify) {
+            mContentDigestsToVerify = contentDigestsToVerify;
+            return this;
+        }
+
+        /**
+         * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
+         * from this instance.
+         *
+         * <note>{@link #verify()} will always verify the content digests for the APK, but this
+         * allows verification of the rotation minimum SDK version stripping attribute to be skipped
+         * for scenarios where this value may not have been parsed from a V3.1 signing block (such
+         * as when only {@link #parseSigners()} will be invoked.</note>
+         */
+        public Builder setFullVerification(boolean fullVerification) {
+            mFullVerification = fullVerification;
+            return this;
+        }
+
+        /**
+         * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
+         * {@code Builder}.
+         */
+        public V3SchemeVerifier build() {
+            int sigSchemeVersion;
+            switch (mBlockId) {
+                case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
+                    sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+                    mMinSdkVersion = Math.max(mMinSdkVersion,
+                            V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
+                    break;
+                case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
+                    sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+                    // V3.1 supports targeting an SDK version later than that of the initial release
+                    // in which it is supported; allow any range for V3.1 as long as V3.0 covers the
+                    // rest of the range.
+                    mMinSdkVersion = mMaxSdkVersion;
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
+                                    mBlockId));
+            }
+            if (mResult == null) {
+                mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
+            }
+            if (mContentDigestsToVerify == null) {
+                mContentDigestsToVerify = new HashSet<>(1);
+            }
+
+            V3SchemeVerifier verifier = new V3SchemeVerifier(
+                    mExecutor,
+                    mApk,
+                    mZipSections,
+                    mContentDigestsToVerify,
+                    mResult,
+                    mMinSdkVersion,
+                    mMaxSdkVersion,
+                    mBlockId,
+                    mOptionalRotationMinSdkVersion,
+                    mFullVerification);
+            if (mApkSignatureSchemeV3Block != null) {
+                verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
+            }
+            return verifier;
+        }
+    }
+}

+ 314 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java

@@ -0,0 +1,314 @@
+/*
+ * 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.apk.v3;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import com.android.apksig.apk.ApkFormatException;
+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.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * APK Signer Lineage.
+ *
+ * <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
+ * the validity of its descendant.  Each additional descendant represents a new identity that can be
+ * used to sign an APK, and each generation has accompanying attributes which represent how the
+ * APK would like to view the older signing certificates, specifically how they should be trusted in
+ * certain situations.
+ *
+ * <p> Its primary use is to enable APK Signing Certificate Rotation.  The Android platform verifies
+ * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
+ * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
+ * allow upgrades to the new certificate.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class V3SigningCertificateLineage {
+
+    private final static int FIRST_VERSION = 1;
+    private final static int CURRENT_VERSION = FIRST_VERSION;
+
+    /**
+     * Deserializes the binary representation of an {@link V3SigningCertificateLineage}. Also
+     * verifies that the structure is well-formed, e.g. that the signature for each node is from its
+     * parent.
+     */
+    public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
+            throws IOException {
+        List<SigningCertificateNode> result = new ArrayList<>();
+        int nodeCount = 0;
+        if (inputBytes == null || !inputBytes.hasRemaining()) {
+            return null;
+        }
+
+        ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes);
+
+        // FORMAT (little endian):
+        // * uint32: version code
+        // * sequence of length-prefixed (uint32): nodes
+        //   * length-prefixed bytes: signed data
+        //     * length-prefixed bytes: certificate
+        //     * uint32: signature algorithm id
+        //   * uint32: flags
+        //   * uint32: signature algorithm id (used by to sign next cert in lineage)
+        //   * length-prefixed bytes: signature over above signed data
+
+        X509Certificate lastCert = null;
+        int lastSigAlgorithmId = 0;
+
+        try {
+            int version = inputBytes.getInt();
+            if (version != CURRENT_VERSION) {
+                // we only have one version to worry about right now, so just check it
+                throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+                        + " different than any of which we are aware");
+            }
+            HashSet<X509Certificate> certHistorySet = new HashSet<>();
+            while (inputBytes.hasRemaining()) {
+                nodeCount++;
+                ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
+                ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
+                int flags = nodeBytes.getInt();
+                int sigAlgorithmId = nodeBytes.getInt();
+                SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
+                byte[] signature = readLengthPrefixedByteArray(nodeBytes);
+
+                if (lastCert != null) {
+                    // Use previous level cert to verify current level
+                    String jcaSignatureAlgorithm =
+                            sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+                    AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                            sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+                    PublicKey publicKey = lastCert.getPublicKey();
+                    Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                    sig.initVerify(publicKey);
+                    if (jcaSignatureAlgorithmParams != null) {
+                        sig.setParameter(jcaSignatureAlgorithmParams);
+                    }
+                    sig.update(signedData);
+                    if (!sig.verify(signature)) {
+                        throw new SecurityException("Unable to verify signature of certificate #"
+                                + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+                                + " V3SigningCertificateLineage object");
+                    }
+                }
+
+                signedData.rewind();
+                byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+                int signedSigAlgorithm = signedData.getInt();
+                if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
+                    throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+                            + nodeBytes + " when verifying V3SigningCertificateLineage object");
+                }
+                lastCert = X509CertificateUtils.generateCertificate(encodedCert);
+                lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
+                if (certHistorySet.contains(lastCert)) {
+                    throw new SecurityException("Encountered duplicate entries in "
+                            + "SigningCertificateLineage at certificate #" + nodeCount + ".  All "
+                            + "signing certificates should be unique");
+                }
+                certHistorySet.add(lastCert);
+                lastSigAlgorithmId = sigAlgorithmId;
+                result.add(new SigningCertificateNode(
+                        lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
+                        SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
+            }
+        } catch(ApkFormatException | BufferUnderflowException e){
+            throw new IOException("Failed to parse V3SigningCertificateLineage object", e);
+        } catch(NoSuchAlgorithmException | InvalidKeyException
+                | InvalidAlgorithmParameterException | SignatureException e){
+            throw new SecurityException(
+                    "Failed to verify signature over signed data for certificate #" + nodeCount
+                            + " when parsing V3SigningCertificateLineage object", e);
+        } catch(CertificateException e){
+            throw new SecurityException("Failed to decode certificate #" + nodeCount
+                    + " when parsing V3SigningCertificateLineage object", e);
+        }
+        return result;
+    }
+
+    /**
+     * encode the in-memory representation of this {@code V3SigningCertificateLineage}
+     */
+    public static byte[] encodeSigningCertificateLineage(
+            List<SigningCertificateNode> signingCertificateLineage) {
+        // FORMAT (little endian):
+        // * version code
+        // * sequence of length-prefixed (uint32): nodes
+        //   * length-prefixed bytes: signed data
+        //     * length-prefixed bytes: certificate
+        //     * uint32: signature algorithm id
+        //   * uint32: flags
+        //   * uint32: signature algorithm id (used by to sign next cert in lineage)
+
+        List<byte[]> nodes = new ArrayList<>();
+        for (SigningCertificateNode node : signingCertificateLineage) {
+            nodes.add(encodeSigningCertificateNode(node));
+        }
+        byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes);
+
+        // add the version code (uint32) on top of the encoded nodes
+        int payloadSize = 4 + encodedSigningCertificateLineage.length;
+        ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize);
+        encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN);
+        encodedWithVersion.putInt(CURRENT_VERSION);
+        encodedWithVersion.put(encodedSigningCertificateLineage);
+        return encodedWithVersion.array();
+    }
+
+    public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) {
+        // FORMAT (little endian):
+        // * length-prefixed bytes: signed data
+        //   * length-prefixed bytes: certificate
+        //   * uint32: signature algorithm id
+        // * uint32: flags
+        // * uint32: signature algorithm id (used by to sign next cert in lineage)
+        // * length-prefixed bytes: signature over signed data
+        int parentSigAlgorithmId = 0;
+        if (node.parentSigAlgorithm != null) {
+            parentSigAlgorithmId = node.parentSigAlgorithm.getId();
+        }
+        int sigAlgorithmId = 0;
+        if (node.sigAlgorithm != null) {
+            sigAlgorithmId = node.sigAlgorithm.getId();
+        }
+        byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId);
+        byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature);
+        int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.put(prefixedSignedData);
+        result.putInt(node.flags);
+        result.putInt(sigAlgorithmId);
+        result.put(prefixedSignature);
+        return result.array();
+    }
+
+    public static byte[] encodeSignedData(X509Certificate certificate, int flags) {
+        try {
+            byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded());
+            int payloadSize = 4 + prefixedCertificate.length;
+            ByteBuffer result = ByteBuffer.allocate(payloadSize);
+            result.order(ByteOrder.LITTLE_ENDIAN);
+            result.put(prefixedCertificate);
+            result.putInt(flags);
+            return encodeAsLengthPrefixedElement(result.array());
+        } catch (CertificateEncodingException e) {
+            throw new RuntimeException(
+                    "Failed to encode V3SigningCertificateLineage certificate", e);
+        }
+    }
+
+    /**
+     * Represents one signing certificate in the {@link V3SigningCertificateLineage}, which
+     * generally means it is/was used at some point to sign the same APK of the others in the
+     * lineage.
+     */
+    public static class SigningCertificateNode {
+
+        public SigningCertificateNode(
+                X509Certificate signingCert,
+                SignatureAlgorithm parentSigAlgorithm,
+                SignatureAlgorithm sigAlgorithm,
+                byte[] signature,
+                int flags) {
+            this.signingCert = signingCert;
+            this.parentSigAlgorithm = parentSigAlgorithm;
+            this.sigAlgorithm = sigAlgorithm;
+            this.signature = signature;
+            this.flags = flags;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof SigningCertificateNode)) return false;
+
+            SigningCertificateNode that = (SigningCertificateNode) o;
+            if (!signingCert.equals(that.signingCert)) return false;
+            if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
+            if (sigAlgorithm != that.sigAlgorithm) return false;
+            if (!Arrays.equals(signature, that.signature)) return false;
+            if (flags != that.flags) return false;
+
+            // we made it
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
+            result = 31 * result + Arrays.hashCode(signature);
+            return result;
+        }
+
+        /**
+         * the signing cert for this node.  This is part of the data signed by the parent node.
+         */
+        public final X509Certificate signingCert;
+
+        /**
+         * the algorithm used by the this node's parent to bless this data.  Its ID value is part of
+         * the data signed by the parent node. {@code null} for first node.
+         */
+        public final SignatureAlgorithm parentSigAlgorithm;
+
+        /**
+         * the algorithm used by the this nodeto bless the next node's data.  Its ID value is part
+         * of the signed data of the next node. {@code null} for the last node.
+         */
+        public SignatureAlgorithm sigAlgorithm;
+
+        /**
+         * signature over the signed data (above).  The signature is from this node's parent
+         * signing certificate, which should correspond to the signing certificate used to sign an
+         * APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
+         */
+        public final byte[] signature;
+
+        /**
+         * the flags detailing how the platform should treat this signing cert
+         */
+        public int flags;
+    }
+}

+ 440 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java

@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2020 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.apk.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
+import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+
+import com.android.apksig.apk.ApkUtils;
+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.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+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.util.Collections;
+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 2 mandatory fields - used during
+ * installation. And optional verity tree - has to be present during session commit.
+ * <p>
+ * The fields:
+ * <p>
+ * 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.
+ * <p>
+ */
+public abstract class V4SchemeSigner {
+    /**
+     * Hidden constructor to prevent instantiation.
+     */
+    private V4SchemeSigner() {
+    }
+
+    public static class SignerConfig {
+        final public ApkSigningBlockUtils.SignerConfig v4Config;
+        final public ApkSigningBlockUtils.SignerConfig v41Config;
+
+        public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs,
+                List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException {
+            if (v4Configs == null || v4Configs.size() != 1) {
+                throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
+            }
+            if (v41Configs != null && v41Configs.size() != 1) {
+                throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature.");
+            }
+            this.v4Config = v4Configs.get(0);
+            this.v41Config = v41Configs != null ? v41Configs.get(0) : null;
+        }
+    }
+
+    /**
+     * Based on a public key, return a signing algorithm that supports verity.
+     */
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean apkSigningBlockPaddingSupported,
+            boolean deterministicDsaSigning)
+            throws InvalidKeyException {
+        List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+                signingKey, minSdkVersion,
+                apkSigningBlockPaddingSupported, deterministicDsaSigning);
+        // Keeping only supported algorithms.
+        for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
+            final SignatureAlgorithm algorithm = iter.next();
+            if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
+                iter.remove();
+            }
+        }
+        return algorithms;
+    }
+
+    /**
+     * Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
+     * output file.
+     */
+    public static void generateV4Signature(
+        DataSource apkContent, SignerConfig signerConfig, File outputFile)
+        throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+      Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
+      try (final OutputStream output = new FileOutputStream(outputFile)) {
+        pair.getFirst().writeTo(output);
+        V4Signature.writeBytes(output, pair.getSecond());
+      } catch (IOException e) {
+        outputFile.delete();
+        throw e;
+      }
+    }
+
+    /** Generate v4 signature and hash tree for a given APK. */
+    public static Pair<V4Signature, byte[]> generateV4Signature(
+            DataSource apkContent,
+            SignerConfig signerConfig)
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+        // 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 the strongest supported digest for each of the v2/v3/v3.1 blocks
+        // (CHUNKED_SHA256 or CHUNKED_SHA512).
+        final Map<Integer, byte[]> apkDigests = getApkDigests(apkContent);
+
+        // 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 {
+            signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData,
+                    fileSize);
+        } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
+            throw new InvalidKeyException("Signer failed", e);
+        }
+
+        return Pair.of(signature, tree);
+    }
+
+    private static V4Signature.SigningInfo generateSigningInfo(
+            ApkSigningBlockUtils.SignerConfig signerConfig,
+            V4Signature.HashingInfo hashingInfo,
+            byte[] apkDigest, byte[] additionalData, long fileSize)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+            CertificateEncodingException {
+        if (signerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+        if (signerConfig.certificates.size() != 1) {
+            throw new CertificateEncodingException("Should only have one certificate");
+        }
+
+        // Collecting data for signing.
+        final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+        final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
+        final byte[] encodedCertificate = encodedCertificates.get(0);
+
+        final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
+                encodedCertificate, additionalData, publicKey.getEncoded(), -1, null);
+
+        final byte[] data = V4Signature.getSignedData(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");
+        }
+
+        final int signatureAlgorithmId = signatures.get(0).getFirst();
+        final byte[] signature = signatures.get(0).getSecond();
+
+        return new V4Signature.SigningInfo(apkDigest,
+                encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId,
+                signature);
+    }
+
+    private static V4Signature generateSignature(
+            SignerConfig signerConfig,
+            V4Signature.HashingInfo hashingInfo,
+            Map<Integer, byte[]> apkDigests, byte[] additionalData, long fileSize)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+            CertificateEncodingException {
+        byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3)
+                ? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3)
+                    : apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2);
+        final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
+                hashingInfo, apkDigest, additionalData, fileSize);
+
+        final V4Signature.SigningInfos signingInfos;
+        if (signerConfig.v41Config != null) {
+            if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) {
+                throw new IllegalStateException(
+                        "V4.1 cannot be signed without a V3.1 content digest");
+            }
+            apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31);
+            final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
+                    APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
+                    generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
+                            additionalData, fileSize).toByteArray());
+            signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
+        } else {
+            signingInfos = new V4Signature.SigningInfos(signingInfo);
+        }
+
+        return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
+                signingInfos.toByteArray());
+    }
+
+    /**
+     * Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the
+     * strongest supported content digest found in that version's signature block for the V2,
+     * V3, and V3.1 signatures in the provided {@code apk}.
+     *
+     * <p>If a supported content digest algorithm is not found in any of the signature blocks,
+     * or if the APK is not signed by any of these signature schemes, then an {@code IOException}
+     * is thrown.
+     */
+    private static Map<Integer, byte[]> getApkDigests(DataSource apk) throws IOException {
+        ApkUtils.ZipSections zipSections;
+        try {
+            zipSections = ApkUtils.findZipSections(apk);
+        } catch (ZipFormatException e) {
+            throw new IOException("Malformed APK: not a ZIP archive", e);
+        }
+
+        Map<Integer, byte[]> sigSchemeToDigest = new HashMap<>(1);
+        try {
+            byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31);
+            sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest);
+        } catch (SignatureException expected) {
+            // It is expected to catch a SignatureException if the APK does not have a v3.1
+            // signature.
+        }
+
+        SignatureException v3Exception = null;
+        try {
+            byte[] digest =  getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3);
+            sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest);
+        } catch (SignatureException e) {
+            v3Exception = e;
+        }
+
+        SignatureException v2Exception = null;
+        try {
+            byte[] digest = getBestV2Digest(apk, zipSections);
+            sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest);
+        } catch (SignatureException e) {
+            v2Exception = e;
+        }
+
+        if (sigSchemeToDigest.size() > 0) {
+            return sigSchemeToDigest;
+        }
+
+        throw new IOException(
+                "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
+                        + v2Exception);
+    }
+
+    private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections,
+            int v3SchemeVersion) throws SignatureException {
+        final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                v3SchemeVersion);
+        final int blockId;
+        switch (v3SchemeVersion) {
+            case VERSION_APK_SIGNATURE_SCHEME_V31:
+                blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+                break;
+            case VERSION_APK_SIGNATURE_SCHEME_V3:
+                blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid V3 scheme provided: " + v3SchemeVersion);
+        }
+        try {
+            final SignatureInfo signatureInfo =
+                    ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result);
+            final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+            V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
+                    result);
+        } catch (Exception e) {
+            throw new SignatureException("Failed to extract and parse v3 block", e);
+        }
+
+        if (result.signers.size() != 1) {
+            throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+        }
+
+        ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+        if (signer.containsErrors()) {
+            throw new SignatureException("Parsing failed: " + signer.getErrors());
+        }
+
+        final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+                result.signers.get(0).contentDigests;
+        return pickBestDigest(contentDigests);
+    }
+
+    private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
+            throws SignatureException {
+        final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
+        final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+        try {
+            final SignatureInfo signatureInfo =
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
+            final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
+            V2SchemeVerifier.parseSigners(
+                    apkSignatureSchemeV2Block,
+                    contentDigestsToVerify,
+                    Collections.emptyMap(),
+                    foundApkSigSchemeIds,
+                    Integer.MAX_VALUE,
+                    Integer.MAX_VALUE,
+                    result);
+        } catch (Exception e) {
+            throw new SignatureException("Failed to extract and parse v2 block", e);
+        }
+
+        if (result.signers.size() != 1) {
+            throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+        }
+
+        ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+        if (signer.containsErrors()) {
+            throw new SignatureException("Parsing failed: " + signer.getErrors());
+        }
+
+        final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+                signer.contentDigests;
+        return pickBestDigest(contentDigests);
+    }
+
+    private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
+        if (contentDigests == null || contentDigests.isEmpty()) {
+            throw new SignatureException("Should have at least one digest");
+        }
+
+        int bestAlgorithmOrder = -1;
+        byte[] bestDigest = null;
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+            final SignatureAlgorithm signatureAlgorithm =
+                    SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+            final ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            if (!isSupported(contentDigestAlgorithm, true)) {
+                continue;
+            }
+            final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
+            if (bestAlgorithmOrder < algorithmOrder) {
+                bestAlgorithmOrder = algorithmOrder;
+                bestDigest = contentDigest.getValue();
+            }
+        }
+        if (bestDigest == null) {
+            throw new SignatureException("Failed to find a supported digest in the source APK");
+        }
+        return bestDigest;
+    }
+
+    public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
+        switch (contentDigestAlgorithm) {
+            case CHUNKED_SHA256:
+                return 0;
+            case VERITY_CHUNKED_SHA256:
+                return 1;
+            case CHUNKED_SHA512:
+                return 2;
+            default:
+                return -1;
+        }
+    }
+
+    private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
+            boolean forV3Digest) {
+        if (contentDigestAlgorithm == null) {
+            return false;
+        }
+        if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+                || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
+                || (forV3Digest
+                     && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+            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.");
+        }
+    }
+}

+ 267 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java

@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2020 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.apk.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
+
+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.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+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;
+
+/**
+ * APK Signature Scheme V4 verifier.
+ * <p>
+ * Verifies the serialized V4Signature file against an APK.
+ */
+public abstract class V4SchemeVerifier {
+    /**
+     * 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.
+     * </p>
+     */
+    public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
+            throws IOException, NoSuchAlgorithmException {
+        final V4Signature signature;
+        final byte[] tree;
+        try (InputStream input = new FileInputStream(v4SignatureFile)) {
+            signature = V4Signature.readFrom(input);
+            tree = V4Signature.readBytes(input);
+        }
+
+        final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+
+        if (signature == null) {
+            result.addError(Issue.V4_SIG_NO_SIGNATURES,
+                    "Signature file does not contain a v4 signature.");
+            return result;
+        }
+
+        if (signature.version != V4Signature.CURRENT_VERSION) {
+            result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
+                    V4Signature.CURRENT_VERSION);
+        }
+
+        V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
+                signature.hashingInfo);
+
+        V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
+                signature.signingInfos);
+
+        final ApkSigningBlockUtils.Result.SignerInfo signerInfo;
+
+        // Verify the primary signature over signedData.
+        {
+            V4Signature.SigningInfo signingInfo = signingInfos.signingInfo;
+            final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+                    signingInfo);
+            signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData);
+            result.signers.add(signerInfo);
+            if (result.containsErrors()) {
+                return result;
+            }
+        }
+
+        // Verify all subsequent signatures.
+        for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
+            V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+                    signingInfoBlock.signingInfo);
+            final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+                    signingInfo);
+            result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData));
+            if (result.containsErrors()) {
+                return result;
+            }
+        }
+
+        // Check if the root hash and the tree are correct.
+        verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
+        if (!result.containsErrors()) {
+            result.verified = true;
+        }
+
+        return 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(
+            V4Signature.SigningInfo signingInfo,
+            final byte[] signedData) throws NoSuchAlgorithmException {
+        final ApkSigningBlockUtils.Result.SignerInfo result =
+                new ApkSigningBlockUtils.Result.SignerInfo();
+        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 {
+            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);
+            }
+            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 (signingInfo.certificate == null) {
+            result.addError(Issue.V4_SIG_NO_CERTIFICATE);
+            return result;
+        }
+
+        final X509Certificate certificate;
+        try {
+            // 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);
+
+        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 apk 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.apkDigest);
+        result.contentDigests.add(contentDigest);
+
+        return result;
+    }
+
+    private static void verifyRootHashAndTree(DataSource apkContent,
+            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;
+        }
+        // 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);
+    }
+}

+ 311 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java

@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2020 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.apk.v4;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class V4Signature {
+    public static final int CURRENT_VERSION = 2;
+
+    public static final int HASHING_ALGORITHM_SHA256 = 1;
+    public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
+
+    public static final int MAX_SIGNING_INFOS_SIZE = 7168;
+
+    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[] apkDigest;  // 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[] apkDigest, byte[] certificate, byte[] additionalData,
+                byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
+            this.apkDigest = apkDigest;
+            this.certificate = certificate;
+            this.additionalData = additionalData;
+            this.publicKey = publicKey;
+            this.signatureAlgorithmId = signatureAlgorithmId;
+            this.signature = signature;
+        }
+
+        static SigningInfo fromByteArray(byte[] bytes) throws IOException {
+            return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
+        }
+
+        static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException {
+            byte[] apkDigest = 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(apkDigest, certificate, additionalData, publicKey,
+                    signatureAlgorithmId, signature);
+        }
+
+        byte[] toByteArray() {
+            final int size = bytesSize(this.apkDigest) + 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.apkDigest);
+            writeBytes(buffer, this.certificate);
+            writeBytes(buffer, this.additionalData);
+            writeBytes(buffer, this.publicKey);
+            buffer.putInt(this.signatureAlgorithmId);
+            writeBytes(buffer, this.signature);
+            return buffer.array();
+        }
+    }
+
+    public static class SigningInfoBlock {
+        public final int blockId;
+        public final byte[] signingInfo;
+
+        public SigningInfoBlock(int blockId, byte[] signingInfo) {
+            this.blockId = blockId;
+            this.signingInfo = signingInfo;
+        }
+
+        static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException {
+            int blockId = buffer.getInt();
+            byte[] signingInfo = readBytes(buffer);
+            return new SigningInfoBlock(blockId, signingInfo);
+        }
+
+        byte[] toByteArray() {
+            final int size = 4/*blockId*/ + bytesSize(this.signingInfo);
+            ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+            buffer.putInt(this.blockId);
+            writeBytes(buffer, this.signingInfo);
+            return buffer.array();
+        }
+    }
+
+    public static class SigningInfos {
+        public final SigningInfo signingInfo;
+        public final SigningInfoBlock[] signingInfoBlocks;
+
+        public SigningInfos(SigningInfo signingInfo) {
+            this.signingInfo = signingInfo;
+            this.signingInfoBlocks = new SigningInfoBlock[0];
+        }
+
+        public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) {
+            this.signingInfo = signingInfo;
+            this.signingInfoBlocks = signingInfoBlocks;
+        }
+
+        public static SigningInfos fromByteArray(byte[] bytes) throws IOException {
+            ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+            SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer);
+            if (!buffer.hasRemaining()) {
+                return new SigningInfos(signingInfo);
+            }
+            ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1);
+            while (buffer.hasRemaining()) {
+                signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer));
+            }
+            return new SigningInfos(signingInfo,
+                    signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()]));
+        }
+
+        byte[] toByteArray() {
+            byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][];
+            arrays[0] = this.signingInfo.toByteArray();
+            int size = arrays[0].length;
+            for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+                arrays[i + 1] = this.signingInfoBlocks[i].toByteArray();
+                size += arrays[i + 1].length;
+            }
+            if (size > MAX_SIGNING_INFOS_SIZE) {
+                throw new IllegalArgumentException(
+                        "Combined SigningInfos length exceeded limit of 7K: " + size);
+            }
+
+            // Combine all arrays into one.
+            byte[] result = Arrays.copyOf(arrays[0], size);
+            int offset = arrays[0].length;
+            for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+                System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length);
+                offset += arrays[i + 1].length;
+            }
+            return result;
+        }
+    }
+
+    // Always 2 for now.
+    public final int version;
+    public final byte[] hashingInfo;
+    // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock.
+    // Passed as-is to the kernel. Can be retrieved later.
+    public final byte[] signingInfos;
+
+    V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) {
+        this.version = version;
+        this.hashingInfo = hashingInfo;
+        this.signingInfos = signingInfos;
+    }
+
+    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);
+    }
+
+    public void writeTo(OutputStream stream) throws IOException {
+        writeIntLE(stream, this.version);
+        writeBytes(stream, this.hashingInfo);
+        writeBytes(stream, this.signingInfos);
+    }
+
+    static byte[] getSignedData(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.apkDigest) + 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.apkDigest);
+        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);
+    }
+
+    static void writeBytes(ByteBuffer buffer, byte[] bytes) {
+        if (bytes == null) {
+            buffer.putInt(0);
+            return;
+        }
+        buffer.putInt(bytes.length);
+        buffer.put(bytes);
+    }
+}

+ 673 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java

@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import com.android.apksig.internal.asn1.ber.BerDataValue;
+import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
+import com.android.apksig.internal.asn1.ber.BerDataValueReader;
+import com.android.apksig.internal.asn1.ber.BerEncoding;
+import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
+import com.android.apksig.internal.util.ByteBufferUtils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parser of ASN.1 BER-encoded structures.
+ *
+ * <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class},
+ * containing fields annotated with {@link Asn1Field}.
+ */
+public final class Asn1BerParser {
+    private Asn1BerParser() {}
+
+    /**
+     * Returns the ASN.1 structure contained in the BER encoded input.
+     *
+     * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
+     *        is advanced to the first position following the end of the consumed structure.
+     * @param containerClass class describing the structure of the input. The class must meet the
+     *        following requirements:
+     *        <ul>
+     *        <li>The class must be annotated with {@link Asn1Class}.</li>
+     *        <li>The class must expose a public no-arg constructor.</li>
+     *        <li>Member fields of the class which are populated with parsed input must be
+     *            annotated with {@link Asn1Field} and be public and non-final.</li>
+     *        </ul>
+     *
+     * @throws Asn1DecodingException if the input could not be decoded into the specified Java
+     *         object
+     */
+    public static <T> T parse(ByteBuffer encoded, Class<T> containerClass)
+            throws Asn1DecodingException {
+        BerDataValue containerDataValue;
+        try {
+            containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
+        } catch (BerDataValueFormatException e) {
+            throw new Asn1DecodingException("Failed to decode top-level data value", e);
+        }
+        if (containerDataValue == null) {
+            throw new Asn1DecodingException("Empty input");
+        }
+        return parse(containerDataValue, containerClass);
+    }
+
+    /**
+     * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
+     * that this method does not care whether the tag number of this data structure is
+     * {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
+     *
+     * <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1
+     * SET may contain duplicate elements.
+     *
+     * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
+     *        is advanced to the first position following the end of the consumed structure.
+     * @param elementClass class describing the structure of the values/elements contained in this
+     *        container. The class must meet the following requirements:
+     *        <ul>
+     *        <li>The class must be annotated with {@link Asn1Class}.</li>
+     *        <li>The class must expose a public no-arg constructor.</li>
+     *        <li>Member fields of the class which are populated with parsed input must be
+     *            annotated with {@link Asn1Field} and be public and non-final.</li>
+     *        </ul>
+     *
+     * @throws Asn1DecodingException if the input could not be decoded into the specified Java
+     *         object
+     */
+    public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
+            throws Asn1DecodingException {
+        BerDataValue containerDataValue;
+        try {
+            containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
+        } catch (BerDataValueFormatException e) {
+            throw new Asn1DecodingException("Failed to decode top-level data value", e);
+        }
+        if (containerDataValue == null) {
+            throw new Asn1DecodingException("Empty input");
+        }
+        return parseSetOf(containerDataValue, elementClass);
+    }
+
+    private static <T> T parse(BerDataValue container, Class<T> containerClass)
+            throws Asn1DecodingException {
+        if (container == null) {
+            throw new NullPointerException("container == null");
+        }
+        if (containerClass == null) {
+            throw new NullPointerException("containerClass == null");
+        }
+
+        Asn1Type dataType = getContainerAsn1Type(containerClass);
+        switch (dataType) {
+            case CHOICE:
+                return parseChoice(container, containerClass);
+
+            case SEQUENCE:
+            {
+                int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL;
+                int expectedTagNumber = BerEncoding.getTagNumber(dataType);
+                if ((container.getTagClass() != expectedTagClass)
+                        || (container.getTagNumber() != expectedTagNumber)) {
+                    throw new Asn1UnexpectedTagException(
+                            "Unexpected data value read as " + containerClass.getName()
+                                    + ". Expected " + BerEncoding.tagClassAndNumberToString(
+                                    expectedTagClass, expectedTagNumber)
+                                    + ", 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");
+        }
+    }
+
+    private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass)
+            throws Asn1DecodingException {
+        List<AnnotatedField> fields = getAnnotatedFields(containerClass);
+        if (fields.isEmpty()) {
+            throw new Asn1DecodingException(
+                    "No fields annotated with " + Asn1Field.class.getName()
+                            + " in CHOICE class " + containerClass.getName());
+        }
+
+        // Check that class + tagNumber don't clash between the choices
+        for (int i = 0; i < fields.size() - 1; i++) {
+            AnnotatedField f1 = fields.get(i);
+            int tagNumber1 = f1.getBerTagNumber();
+            int tagClass1 = f1.getBerTagClass();
+            for (int j = i + 1; j < fields.size(); j++) {
+                AnnotatedField f2 = fields.get(j);
+                int tagNumber2 = f2.getBerTagNumber();
+                int tagClass2 = f2.getBerTagClass();
+                if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) {
+                    throw new Asn1DecodingException(
+                            "CHOICE fields are indistinguishable because they have the same tag"
+                                    + " class and number: " + containerClass.getName()
+                                    + "." + f1.getField().getName()
+                                    + " and ." + f2.getField().getName());
+                }
+            }
+        }
+
+        // Instantiate the container object / result
+        T obj;
+        try {
+            obj = containerClass.getConstructor().newInstance();
+        } 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 {
+                field.setValueFrom(dataValue, obj);
+                return obj;
+            } catch (Asn1UnexpectedTagException expected) {
+                // not a match
+            }
+        }
+
+        throw new Asn1DecodingException(
+                "No options of CHOICE " + containerClass.getName() + " matched");
+    }
+
+    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());
+        // Check that there are no fields with the same index
+        if (fields.size() > 1) {
+            AnnotatedField lastField = null;
+            for (AnnotatedField field : fields) {
+                if ((lastField != null)
+                        && (lastField.getAnnotation().index() == field.getAnnotation().index())) {
+                    throw new Asn1DecodingException(
+                            "Fields have the same index: " + containerClass.getName()
+                                    + "." + lastField.getField().getName()
+                                    + " and ." + field.getField().getName());
+                }
+                lastField = field;
+            }
+        }
+
+        // Instantiate the container object / result
+        T t;
+        try {
+            t = containerClass.getConstructor().newInstance();
+        } catch (IllegalArgumentException | ReflectiveOperationException e) {
+            throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
+        }
+
+        // Parse fields one by one. A complication is that there may be optional fields.
+        int nextUnreadFieldIndex = 0;
+        BerDataValueReader elementsReader = container.contentsReader();
+        while (nextUnreadFieldIndex < fields.size()) {
+            BerDataValue dataValue;
+            try {
+                // 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);
+            }
+            if (dataValue == null) {
+                break;
+            }
+
+            for (int i = nextUnreadFieldIndex; i < fields.size(); i++) {
+                AnnotatedField field = fields.get(i);
+                try {
+                    if (field.isOptional()) {
+                        // Optional field -- might not be present and we may thus be trying to set
+                        // it from the wrong tag.
+                        try {
+                            field.setValueFrom(dataValue, t);
+                            nextUnreadFieldIndex = i + 1;
+                            break;
+                        } catch (Asn1UnexpectedTagException e) {
+                            // This field is not present, attempt to use this data value for the
+                            // next / iteration of the loop
+                            continue;
+                        }
+                    } else {
+                        // Mandatory field -- if we can't set its value from this data value, then
+                        // it's an error
+                        field.setValueFrom(dataValue, t);
+                        nextUnreadFieldIndex = i + 1;
+                        break;
+                    }
+                } catch (Asn1DecodingException e) {
+                    throw new Asn1DecodingException(
+                            "Failed to parse " + containerClass.getName()
+                                    + "." + field.getField().getName(),
+                            e);
+                }
+            }
+        }
+
+        return t;
+    }
+
+    // NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness
+    // of elements -- it's an unordered collection.
+    @SuppressWarnings("unchecked")
+    private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass)
+            throws Asn1DecodingException {
+        List<T> result = new ArrayList<>();
+        BerDataValueReader elementsReader = container.contentsReader();
+        while (true) {
+            BerDataValue dataValue;
+            try {
+                dataValue = elementsReader.readDataValue();
+            } catch (BerDataValueFormatException e) {
+                throw new Asn1DecodingException("Malformed data value", e);
+            }
+            if (dataValue == null) {
+                break;
+            }
+            T element;
+            if (ByteBuffer.class.equals(elementClass)) {
+                element = (T) dataValue.getEncodedContents();
+            } else if (Asn1OpaqueObject.class.equals(elementClass)) {
+                element = (T) new Asn1OpaqueObject(dataValue.getEncoded());
+            } else {
+                element = parse(dataValue, elementClass);
+            }
+            result.add(element);
+        }
+        return result;
+    }
+
+    private static Asn1Type getContainerAsn1Type(Class<?> containerClass)
+            throws Asn1DecodingException {
+        Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
+        if (containerAnnotation == null) {
+            throw new Asn1DecodingException(
+                    containerClass.getName() + " is not annotated with "
+                            + Asn1Class.class.getName());
+        }
+
+        switch (containerAnnotation.type()) {
+            case CHOICE:
+            case SEQUENCE:
+            case UNENCODED_CONTAINER:
+                return containerAnnotation.type();
+            default:
+                throw new Asn1DecodingException(
+                        "Unsupported ASN.1 container annotation type: "
+                                + containerAnnotation.type());
+        }
+    }
+
+    private static Class<?> getElementType(Field field)
+            throws Asn1DecodingException, ClassNotFoundException {
+        String type = field.getGenericType().getTypeName();
+        int delimiterIndex =  type.indexOf('<');
+        if (delimiterIndex == -1) {
+            throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
+        }
+        int startIndex = delimiterIndex + 1;
+        int endIndex = type.indexOf('>', startIndex);
+        // TODO: handle comma?
+        if (endIndex == -1) {
+            throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
+        }
+        String elementClassName = type.substring(startIndex, endIndex);
+        return Class.forName(elementClassName);
+    }
+
+    private static final class AnnotatedField {
+        private final Field mField;
+        private final Asn1Field mAnnotation;
+        private final Asn1Type mDataType;
+        private final Asn1TagClass mTagClass;
+        private final int mBerTagClass;
+        private final int mBerTagNumber;
+        private final Asn1Tagging mTagging;
+        private final boolean mOptional;
+
+        public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException {
+            mField = field;
+            mAnnotation = annotation;
+            mDataType = annotation.type();
+
+            Asn1TagClass tagClass = annotation.cls();
+            if (tagClass == Asn1TagClass.AUTOMATIC) {
+                if (annotation.tagNumber() != -1) {
+                    tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
+                } else {
+                    tagClass = Asn1TagClass.UNIVERSAL;
+                }
+            }
+            mTagClass = tagClass;
+            mBerTagClass = BerEncoding.getTagClass(mTagClass);
+
+            int tagNumber;
+            if (annotation.tagNumber() != -1) {
+                tagNumber = annotation.tagNumber();
+            } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
+                tagNumber = -1;
+            } else {
+                tagNumber = BerEncoding.getTagNumber(mDataType);
+            }
+            mBerTagNumber = tagNumber;
+
+            mTagging = annotation.tagging();
+            if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
+                    && (annotation.tagNumber() == -1)) {
+                throw new Asn1DecodingException(
+                        "Tag number must be specified when tagging mode is " + mTagging);
+            }
+
+            mOptional = annotation.optional();
+        }
+
+        public Field getField() {
+            return mField;
+        }
+
+        public Asn1Field getAnnotation() {
+            return mAnnotation;
+        }
+
+        public boolean isOptional() {
+            return mOptional;
+        }
+
+        public int getBerTagClass() {
+            return mBerTagClass;
+        }
+
+        public int getBerTagNumber() {
+            return mBerTagNumber;
+        }
+
+        public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException {
+            int readTagClass = dataValue.getTagClass();
+            if (mBerTagNumber != -1) {
+                int readTagNumber = dataValue.getTagNumber();
+                if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) {
+                    throw new Asn1UnexpectedTagException(
+                            "Tag mismatch. Expected: "
+                            + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber)
+                            + ", but found "
+                            + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber));
+                }
+            } else {
+                if (readTagClass != mBerTagClass) {
+                    throw new Asn1UnexpectedTagException(
+                            "Tag mismatch. Expected class: "
+                            + BerEncoding.tagClassToString(mBerTagClass)
+                            + ", but found "
+                            + BerEncoding.tagClassToString(readTagClass));
+                }
+            }
+
+            if (mTagging == Asn1Tagging.EXPLICIT) {
+                try {
+                    dataValue = dataValue.contentsReader().readDataValue();
+                } catch (BerDataValueFormatException e) {
+                    throw new Asn1DecodingException(
+                            "Failed to read contents of EXPLICIT data value", e);
+                }
+            }
+
+            BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue);
+        }
+    }
+
+    private static class Asn1UnexpectedTagException extends Asn1DecodingException {
+        private static final long serialVersionUID = 1L;
+
+        public Asn1UnexpectedTagException(String message) {
+            super(message);
+        }
+    }
+
+    private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException {
+        if (!encodedOid.hasRemaining()) {
+            throw new Asn1DecodingException("Empty OBJECT IDENTIFIER");
+        }
+
+        // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2
+        long firstComponent = decodeBase128UnsignedLong(encodedOid);
+        int firstNode = (int) Math.min(firstComponent / 40, 2);
+        long secondNode = firstComponent - firstNode * 40;
+        StringBuilder result = new StringBuilder();
+        result.append(Long.toString(firstNode)).append('.')
+                .append(Long.toString(secondNode));
+
+        // Each consecutive node is encoded as a separate component
+        while (encodedOid.hasRemaining()) {
+            long node = decodeBase128UnsignedLong(encodedOid);
+            result.append('.').append(Long.toString(node));
+        }
+
+        return result.toString();
+    }
+
+    private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException {
+        if (!encoded.hasRemaining()) {
+            return 0;
+        }
+        long result = 0;
+        while (encoded.hasRemaining()) {
+            if (result > Long.MAX_VALUE >>> 7) {
+                throw new Asn1DecodingException("Base-128 number too large");
+            }
+            int b = encoded.get() & 0xff;
+            result <<= 7;
+            result |= b & 0x7f;
+            if ((b & 0x80) == 0) {
+                return result;
+            }
+        }
+        throw new Asn1DecodingException(
+                "Truncated base-128 encoded input: missing terminating byte, with highest bit not"
+                        + " set");
+    }
+
+    private static BigInteger integerToBigInteger(ByteBuffer encoded) {
+        if (!encoded.hasRemaining()) {
+            return BigInteger.ZERO;
+        }
+        return new BigInteger(ByteBufferUtils.toByteArray(encoded));
+    }
+
+    private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
+        BigInteger value = integerToBigInteger(encoded);
+        if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
+            || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
+            throw new Asn1DecodingException(
+                String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
+        }
+        return value.intValue();
+    }
+
+    private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
+        BigInteger value = integerToBigInteger(encoded);
+        if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
+                || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
+            throw new Asn1DecodingException(
+                String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
+        }
+        return value.longValue();
+    }
+
+    private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
+            throws Asn1DecodingException {
+        Field[] declaredFields = containerClass.getDeclaredFields();
+        List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
+        for (Field field : declaredFields) {
+            Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
+            if (annotation == null) {
+                continue;
+            }
+            if (Modifier.isStatic(field.getModifiers())) {
+                throw new Asn1DecodingException(
+                        Asn1Field.class.getName() + " used on a static field: "
+                                + containerClass.getName() + "." + field.getName());
+            }
+
+            AnnotatedField annotatedField;
+            try {
+                annotatedField = new AnnotatedField(field, annotation);
+            } catch (Asn1DecodingException e) {
+                throw new Asn1DecodingException(
+                        "Invalid ASN.1 annotation on "
+                                + containerClass.getName() + "." + field.getName(),
+                        e);
+            }
+            result.add(annotatedField);
+        }
+        return result;
+    }
+
+    private static final class BerToJavaConverter {
+        private BerToJavaConverter() {}
+
+        public static void setFieldValue(
+                Object obj, Field field, Asn1Type type, BerDataValue dataValue)
+                        throws Asn1DecodingException {
+            try {
+                switch (type) {
+                    case SET_OF:
+                    case SEQUENCE_OF:
+                        if (Asn1OpaqueObject.class.equals(field.getType())) {
+                            field.set(obj, convert(type, dataValue, field.getType()));
+                        } else {
+                            field.set(obj, parseSetOf(dataValue, getElementType(field)));
+                        }
+                        return;
+                    default:
+                        field.set(obj, convert(type, dataValue, field.getType()));
+                        break;
+                }
+            } catch (ReflectiveOperationException e) {
+                throw new Asn1DecodingException(
+                        "Failed to set value of " + obj.getClass().getName()
+                                + "." + field.getName(),
+                        e);
+            }
+        }
+
+        private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+        @SuppressWarnings("unchecked")
+        public static <T> T convert(
+                Asn1Type sourceType,
+                BerDataValue dataValue,
+                Class<T> targetType) throws Asn1DecodingException {
+            if (ByteBuffer.class.equals(targetType)) {
+                return (T) dataValue.getEncodedContents();
+            } else if (byte[].class.equals(targetType)) {
+                ByteBuffer resultBuf = dataValue.getEncodedContents();
+                if (!resultBuf.hasRemaining()) {
+                    return (T) EMPTY_BYTE_ARRAY;
+                }
+                byte[] result = new byte[resultBuf.remaining()];
+                resultBuf.get(result);
+                return (T) result;
+            } else if (Asn1OpaqueObject.class.equals(targetType)) {
+                return (T) new Asn1OpaqueObject(dataValue.getEncoded());
+            }
+            ByteBuffer encodedContents = dataValue.getEncodedContents();
+            switch (sourceType) {
+                case INTEGER:
+                    if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
+                        return (T) Integer.valueOf(integerToInt(encodedContents));
+                    } else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
+                        return (T) Long.valueOf(integerToLong(encodedContents));
+                    } else if (BigInteger.class.equals(targetType)) {
+                        return (T) integerToBigInteger(encodedContents);
+                    }
+                    break;
+                case OBJECT_IDENTIFIER:
+                    if (String.class.equals(targetType)) {
+                        return (T) oidToString(encodedContents);
+                    }
+                    break;
+                case UTC_TIME:
+                case GENERALIZED_TIME:
+                    if (String.class.equals(targetType)) {
+                        return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
+                    }
+                    break;
+                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);
+                    }
+                    break;
+                case SEQUENCE:
+                {
+                    Asn1Class containerAnnotation =
+                            targetType.getDeclaredAnnotation(Asn1Class.class);
+                    if ((containerAnnotation != null)
+                            && (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
+                        return parseSequence(dataValue, targetType);
+                    }
+                    break;
+                }
+                case CHOICE:
+                {
+                    Asn1Class containerAnnotation =
+                            targetType.getDeclaredAnnotation(Asn1Class.class);
+                    if ((containerAnnotation != null)
+                            && (containerAnnotation.type() == Asn1Type.CHOICE)) {
+                        return parseChoice(dataValue, targetType);
+                    }
+                    break;
+                }
+                default:
+                    break;
+            }
+
+            throw new Asn1DecodingException(
+                    "Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
+        }
+    }
+}

+ 28 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java

@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Asn1Class {
+    public Asn1Type type();
+}

+ 32 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+/**
+ * Indicates that input could not be decoded into intended ASN.1 structure.
+ */
+public class Asn1DecodingException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public Asn1DecodingException(String message) {
+        super(message);
+    }
+
+    public Asn1DecodingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 596 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java

@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import com.android.apksig.internal.asn1.ber.BerEncoding;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Encoder of ASN.1 structures into DER-encoded form.
+ *
+ * <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class},
+ * containing fields annotated with {@link Asn1Field}.
+ */
+public final class Asn1DerEncoder {
+    private Asn1DerEncoder() {}
+
+    /**
+     * Returns the DER-encoded form of the provided ASN.1 structure.
+     *
+     * @param container container to be encoded. The container's class must meet the following
+     *        requirements:
+     *        <ul>
+     *        <li>The class must be annotated with {@link Asn1Class}.</li>
+     *        <li>Member fields of the class which are to be encoded must be annotated with
+     *            {@link Asn1Field} and be public.</li>
+     *        </ul>
+     *
+     * @throws Asn1EncodingException if the input could not be encoded
+     */
+    public static byte[] encode(Object container) throws Asn1EncodingException {
+        Class<?> containerClass = container.getClass();
+        Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
+        if (containerAnnotation == null) {
+            throw new Asn1EncodingException(
+                    containerClass.getName() + " not annotated with " + Asn1Class.class.getName());
+        }
+
+        Asn1Type containerType = containerAnnotation.type();
+        switch (containerType) {
+            case CHOICE:
+                return toChoice(container);
+            case SEQUENCE:
+                return toSequence(container);
+            case UNENCODED_CONTAINER:
+                return toSequence(container, true);
+            default:
+                throw new Asn1EncodingException("Unsupported container type: " + containerType);
+        }
+    }
+
+    private static byte[] toChoice(Object container) throws Asn1EncodingException {
+        Class<?> containerClass = container.getClass();
+        List<AnnotatedField> fields = getAnnotatedFields(container);
+        if (fields.isEmpty()) {
+            throw new Asn1EncodingException(
+                    "No fields annotated with " + Asn1Field.class.getName()
+                            + " in CHOICE class " + containerClass.getName());
+        }
+
+        AnnotatedField resultField = null;
+        for (AnnotatedField field : fields) {
+            Object fieldValue = getMemberFieldValue(container, field.getField());
+            if (fieldValue != null) {
+                if (resultField != null) {
+                    throw new Asn1EncodingException(
+                            "Multiple non-null fields in CHOICE class " + containerClass.getName()
+                                    + ": " + resultField.getField().getName()
+                                    + ", " + field.getField().getName());
+                }
+                resultField = field;
+            }
+        }
+
+        if (resultField == null) {
+            throw new Asn1EncodingException(
+                    "No non-null fields in CHOICE class " + containerClass.getName());
+        }
+
+        return resultField.toDer();
+    }
+
+    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(
+                fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
+        if (fields.size() > 1) {
+            AnnotatedField lastField = null;
+            for (AnnotatedField field : fields) {
+                if ((lastField != null)
+                        && (lastField.getAnnotation().index() == field.getAnnotation().index())) {
+                    throw new Asn1EncodingException(
+                            "Fields have the same index: " + containerClass.getName()
+                                    + "." + lastField.getField().getName()
+                                    + " and ." + field.getField().getName());
+                }
+                lastField = field;
+            }
+        }
+
+        List<byte[]> serializedFields = new ArrayList<>(fields.size());
+        int contentLen = 0;
+        for (AnnotatedField field : fields) {
+            byte[] serializedField;
+            try {
+                serializedField = field.toDer();
+            } catch (Asn1EncodingException e) {
+                throw new Asn1EncodingException(
+                        "Failed to encode " + containerClass.getName()
+                                + "." + field.getField().getName(),
+                        e);
+            }
+            if (serializedField != null) {
+                serializedFields.add(serializedField);
+                contentLen += serializedField.length;
+            }
+        }
+
+        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) 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));
+        }
+        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, tagNumber,
+                serializedValues.toArray(new byte[0][]));
+    }
+
+    /**
+     * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the
+     * two arrays are compared in ascending order. Elements at out of range indices are assumed to
+     * be smaller than the smallest possible value for an element.
+     */
+    private static class ByteArrayLexicographicComparator implements Comparator<byte[]> {
+            private static final ByteArrayLexicographicComparator INSTANCE =
+                    new ByteArrayLexicographicComparator();
+
+            @Override
+            public int compare(byte[] arr1, byte[] arr2) {
+                int commonLength = Math.min(arr1.length, arr2.length);
+                for (int i = 0; i < commonLength; i++) {
+                    int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff);
+                    if (diff != 0) {
+                        return diff;
+                    }
+                }
+                return arr1.length - arr2.length;
+            }
+    }
+
+    private static List<AnnotatedField> getAnnotatedFields(Object container)
+            throws Asn1EncodingException {
+        Class<?> containerClass = container.getClass();
+        Field[] declaredFields = containerClass.getDeclaredFields();
+        List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
+        for (Field field : declaredFields) {
+            Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
+            if (annotation == null) {
+                continue;
+            }
+            if (Modifier.isStatic(field.getModifiers())) {
+                throw new Asn1EncodingException(
+                        Asn1Field.class.getName() + " used on a static field: "
+                                + containerClass.getName() + "." + field.getName());
+            }
+
+            AnnotatedField annotatedField;
+            try {
+                annotatedField = new AnnotatedField(container, field, annotation);
+            } catch (Asn1EncodingException e) {
+                throw new Asn1EncodingException(
+                        "Invalid ASN.1 annotation on "
+                                + containerClass.getName() + "." + field.getName(),
+                        e);
+            }
+            result.add(annotatedField);
+        }
+        return result;
+    }
+
+    private static byte[] toInteger(int value) {
+        return toInteger((long) value);
+    }
+
+    private static byte[] toInteger(long value) {
+        return toInteger(BigInteger.valueOf(value));
+    }
+
+    private static byte[] toInteger(BigInteger value) {
+        return createTag(
+                BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
+                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("\\.");
+        if (nodes.length < 2) {
+            throw new Asn1EncodingException(
+                    "OBJECT IDENTIFIER must contain at least two nodes: " + oid);
+        }
+        int firstNode;
+        try {
+            firstNode = Integer.parseInt(nodes[0]);
+        } catch (NumberFormatException e) {
+            throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
+        }
+        if ((firstNode > 6) || (firstNode < 0)) {
+            throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
+        }
+
+        int secondNode;
+        try {
+            secondNode = Integer.parseInt(nodes[1]);
+        } catch (NumberFormatException e) {
+            throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
+        }
+        if ((secondNode >= 40) || (secondNode < 0)) {
+            throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
+        }
+        int firstByte = firstNode * 40 + secondNode;
+        if (firstByte > 0xff) {
+            throw new Asn1EncodingException(
+                    "First two nodes out of range: " + firstNode + "." + secondNode);
+        }
+
+        encodedValue.write(firstByte);
+        for (int i = 2; i < nodes.length; i++) {
+            String nodeString = nodes[i];
+            int node;
+            try {
+                node = Integer.parseInt(nodeString);
+            } catch (NumberFormatException e) {
+                throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
+            }
+            if (node < 0) {
+                throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
+            }
+            if (node <= 0x7f) {
+                encodedValue.write(node);
+                continue;
+            }
+            if (node < 1 << 14) {
+                encodedValue.write(0x80 | (node >> 7));
+                encodedValue.write(node & 0x7f);
+                continue;
+            }
+            if (node < 1 << 21) {
+                encodedValue.write(0x80 | (node >> 14));
+                encodedValue.write(0x80 | ((node >> 7) & 0x7f));
+                encodedValue.write(node & 0x7f);
+                continue;
+            }
+            throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
+        }
+
+        return createTag(
+                BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
+                encodedValue.toByteArray());
+    }
+
+    private static Object getMemberFieldValue(Object obj, Field field)
+            throws Asn1EncodingException {
+        try {
+            return field.get(obj);
+        } catch (ReflectiveOperationException e) {
+            throw new Asn1EncodingException(
+                    "Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
+        }
+    }
+
+    private static final class AnnotatedField {
+        private final Field mField;
+        private final Object mObject;
+        private final Asn1Field mAnnotation;
+        private final Asn1Type mDataType;
+        private final Asn1Type mElementDataType;
+        private final Asn1TagClass mTagClass;
+        private final int mDerTagClass;
+        private final int mDerTagNumber;
+        private final Asn1Tagging mTagging;
+        private final boolean mOptional;
+
+        public AnnotatedField(Object obj, Field field, Asn1Field annotation)
+                throws Asn1EncodingException {
+            mObject = obj;
+            mField = field;
+            mAnnotation = annotation;
+            mDataType = annotation.type();
+            mElementDataType = annotation.elementType();
+
+            Asn1TagClass tagClass = annotation.cls();
+            if (tagClass == Asn1TagClass.AUTOMATIC) {
+                if (annotation.tagNumber() != -1) {
+                    tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
+                } else {
+                    tagClass = Asn1TagClass.UNIVERSAL;
+                }
+            }
+            mTagClass = tagClass;
+            mDerTagClass = BerEncoding.getTagClass(mTagClass);
+
+            int tagNumber;
+            if (annotation.tagNumber() != -1) {
+                tagNumber = annotation.tagNumber();
+            } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
+                tagNumber = -1;
+            } else {
+                tagNumber = BerEncoding.getTagNumber(mDataType);
+            }
+            mDerTagNumber = tagNumber;
+
+            mTagging = annotation.tagging();
+            if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
+                    && (annotation.tagNumber() == -1)) {
+                throw new Asn1EncodingException(
+                        "Tag number must be specified when tagging mode is " + mTagging);
+            }
+
+            mOptional = annotation.optional();
+        }
+
+        public Field getField() {
+            return mField;
+        }
+
+        public Asn1Field getAnnotation() {
+            return mAnnotation;
+        }
+
+        public byte[] toDer() throws Asn1EncodingException {
+            Object fieldValue = getMemberFieldValue(mObject, mField);
+            if (fieldValue == null) {
+                if (mOptional) {
+                    return null;
+                }
+                throw new Asn1EncodingException("Required field not set");
+            }
+
+            byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
+            switch (mTagging) {
+                case NORMAL:
+                    return encoded;
+                case EXPLICIT:
+                    return createTag(mDerTagClass, true, mDerTagNumber, encoded);
+                case IMPLICIT:
+                    int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
+                    if (originalTagNumber == 0x1f) {
+                        throw new Asn1EncodingException("High-tag-number form not supported");
+                    }
+                    if (mDerTagNumber >= 0x1f) {
+                        throw new Asn1EncodingException(
+                                "Unsupported high tag number: " + mDerTagNumber);
+                    }
+                    encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
+                    encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
+                    return encoded;
+                default:
+                    throw new RuntimeException("Unknown tagging mode: " + mTagging);
+            }
+        }
+    }
+
+    private static byte[] createTag(
+            int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
+        if (tagNumber >= 0x1f) {
+            throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
+        }
+        // tag class & number fit into the first byte
+        byte firstIdentifierByte =
+                (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
+
+        int contentsLength = 0;
+        for (byte[] c : contents) {
+            contentsLength += c.length;
+        }
+        int contentsPosInResult;
+        byte[] result;
+        if (contentsLength < 0x80) {
+            // Length fits into one byte
+            contentsPosInResult = 2;
+            result = new byte[contentsPosInResult + contentsLength];
+            result[0] = firstIdentifierByte;
+            result[1] = (byte) contentsLength;
+        } else {
+            // Length is represented as multiple bytes
+            // The low 7 bits of the first byte represent the number of length bytes (following the
+            // first byte) in which the length is in big-endian base-256 form
+            if (contentsLength <= 0xff) {
+                contentsPosInResult = 3;
+                result = new byte[contentsPosInResult + contentsLength];
+                result[1] = (byte) 0x81; // 1 length byte
+                result[2] = (byte) contentsLength;
+            } else if (contentsLength <= 0xffff) {
+                contentsPosInResult = 4;
+                result = new byte[contentsPosInResult + contentsLength];
+                result[1] = (byte) 0x82; // 2 length bytes
+                result[2] = (byte) (contentsLength >> 8);
+                result[3] = (byte) (contentsLength & 0xff);
+            } else if (contentsLength <= 0xffffff) {
+                contentsPosInResult = 5;
+                result = new byte[contentsPosInResult + contentsLength];
+                result[1] = (byte) 0x83; // 3 length bytes
+                result[2] = (byte) (contentsLength >> 16);
+                result[3] = (byte) ((contentsLength >> 8) & 0xff);
+                result[4] = (byte) (contentsLength & 0xff);
+            } else {
+                contentsPosInResult = 6;
+                result = new byte[contentsPosInResult + contentsLength];
+                result[1] = (byte) 0x84; // 4 length bytes
+                result[2] = (byte) (contentsLength >> 24);
+                result[3] = (byte) ((contentsLength >> 16) & 0xff);
+                result[4] = (byte) ((contentsLength >> 8) & 0xff);
+                result[5] = (byte) (contentsLength & 0xff);
+            }
+            result[0] = firstIdentifierByte;
+        }
+        for (byte[] c : contents) {
+            System.arraycopy(c, 0, result, contentsPosInResult, c.length);
+            contentsPosInResult += c.length;
+        }
+        return result;
+    }
+
+    private static final class JavaToDerConverter {
+        private JavaToDerConverter() {}
+
+        public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType)
+                throws Asn1EncodingException {
+            Class<?> sourceType = source.getClass();
+            if (Asn1OpaqueObject.class.equals(sourceType)) {
+                ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
+                byte[] result = new byte[buf.remaining()];
+                buf.get(result);
+                return result;
+            }
+
+            if ((targetType == null) || (targetType == Asn1Type.ANY)) {
+                return encode(source);
+            }
+
+            switch (targetType) {
+                case OCTET_STRING:
+                case BIT_STRING:
+                    byte[] value = null;
+                    if (source instanceof ByteBuffer) {
+                        ByteBuffer buf = (ByteBuffer) source;
+                        value = new byte[buf.remaining()];
+                        buf.slice().get(value);
+                    } else if (source instanceof byte[]) {
+                        value = (byte[]) source;
+                    }
+                    if (value != null) {
+                        return createTag(
+                                BerEncoding.TAG_CLASS_UNIVERSAL,
+                                false,
+                                BerEncoding.getTagNumber(targetType),
+                                value);
+                    }
+                    break;
+                case INTEGER:
+                    if (source instanceof Integer) {
+                        return toInteger((Integer) source);
+                    } else if (source instanceof Long) {
+                        return toInteger((Long) source);
+                    } else if (source instanceof BigInteger) {
+                        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());
+                    }
+                    break;
+                case OBJECT_IDENTIFIER:
+                    if (source instanceof String) {
+                        return toOid((String) source);
+                    }
+                    break;
+                case SEQUENCE:
+                {
+                    Asn1Class containerAnnotation =
+                            sourceType.getDeclaredAnnotation(Asn1Class.class);
+                    if ((containerAnnotation != null)
+                            && (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
+                        return toSequence(source);
+                    }
+                    break;
+                }
+                case CHOICE:
+                {
+                    Asn1Class containerAnnotation =
+                            sourceType.getDeclaredAnnotation(Asn1Class.class);
+                    if ((containerAnnotation != null)
+                            && (containerAnnotation.type() == Asn1Type.CHOICE)) {
+                        return toChoice(source);
+                    }
+                    break;
+                }
+                case SET_OF:
+                    return toSetOf((Collection<?>) source, targetElementType);
+                case SEQUENCE_OF:
+                    return toSequenceOf((Collection<?>) source, targetElementType);
+                default:
+                    break;
+            }
+
+            throw new Asn1EncodingException(
+                    "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
+        }
+    }
+    /** ASN.1 DER-encoded {@code NULL}. */
+    public static final Asn1OpaqueObject ASN1_DER_NULL =
+            new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
+}

+ 32 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+/**
+ * Indicates that an ASN.1 structure could not be encoded.
+ */
+public class Asn1EncodingException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public Asn1EncodingException(String message) {
+        super(message);
+    }
+
+    public Asn1EncodingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 45 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Asn1Field {
+    /** Index used to order fields in a container. Required for fields of SEQUENCE containers. */
+    public int index() default 0;
+
+    public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
+
+    public Asn1Type type();
+
+    /** Tagging mode. Default: NORMAL. */
+    public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
+
+    /** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/
+    public int tagNumber() default -1;
+
+    /** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */
+    public boolean optional() default false;
+
+    /** Type of elements. Used only for SET_OF or SEQUENCE_OF. */
+    public Asn1Type elementType() default Asn1Type.ANY;
+}

+ 38 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Opaque holder of encoded ASN.1 stuff.
+ */
+public class Asn1OpaqueObject {
+    private final ByteBuffer mEncoded;
+
+    public Asn1OpaqueObject(ByteBuffer encoded) {
+        mEncoded = encoded.slice();
+    }
+
+    public Asn1OpaqueObject(byte[] encoded) {
+        mEncoded = ByteBuffer.wrap(encoded);
+    }
+
+    public ByteBuffer getEncoded() {
+        return mEncoded.slice();
+    }
+}

+ 30 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+public enum Asn1TagClass {
+    UNIVERSAL,
+    APPLICATION,
+    CONTEXT_SPECIFIC,
+    PRIVATE,
+
+    /**
+     * Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
+     * automatically.
+     */
+    AUTOMATIC,
+}

+ 23 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+public enum Asn1Tagging {
+    NORMAL,
+    EXPLICIT,
+    IMPLICIT,
+}

+ 35 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+public enum Asn1Type {
+    ANY,
+    CHOICE,
+    INTEGER,
+    OBJECT_IDENTIFIER,
+    OCTET_STRING,
+    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
+}

+ 115 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java

@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
+ */
+public class BerDataValue {
+    private final ByteBuffer mEncoded;
+    private final ByteBuffer mEncodedContents;
+    private final int mTagClass;
+    private final boolean mConstructed;
+    private final int mTagNumber;
+
+    BerDataValue(
+            ByteBuffer encoded,
+            ByteBuffer encodedContents,
+            int tagClass,
+            boolean constructed,
+            int tagNumber) {
+        mEncoded = encoded;
+        mEncodedContents = encodedContents;
+        mTagClass = tagClass;
+        mConstructed = constructed;
+        mTagNumber = tagNumber;
+    }
+
+    /**
+     * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
+     * constants.
+     */
+    public int getTagClass() {
+        return mTagClass;
+    }
+
+    /**
+     * Returns {@code true} if the content octets of this data value are the complete BER encoding
+     * of one or more data values, {@code false} if the content octets of this data value directly
+     * represent the value.
+     */
+    public boolean isConstructed() {
+        return mConstructed;
+    }
+
+    /**
+     * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
+     * constants.
+     */
+    public int getTagNumber() {
+        return mTagNumber;
+    }
+
+    /**
+     * Returns the encoded form of this data value.
+     */
+    public ByteBuffer getEncoded() {
+        return mEncoded.slice();
+    }
+
+    /**
+     * Returns the encoded contents of this data value.
+     */
+    public ByteBuffer getEncodedContents() {
+        return mEncodedContents.slice();
+    }
+
+    /**
+     * Returns a new reader of the contents of this data value.
+     */
+    public BerDataValueReader contentsReader() {
+        return new ByteBufferBerDataValueReader(getEncodedContents());
+    }
+
+    /**
+     * Returns a new reader which returns just this data value. This may be useful for re-reading
+     * this value in different contexts.
+     */
+    public BerDataValueReader dataValueReader() {
+        return new ParsedValueReader(this);
+    }
+
+    private static final class ParsedValueReader implements BerDataValueReader {
+        private final BerDataValue mValue;
+        private boolean mValueOutput;
+
+        public ParsedValueReader(BerDataValue value) {
+            mValue = value;
+        }
+
+        @Override
+        public BerDataValue readDataValue() throws BerDataValueFormatException {
+            if (mValueOutput) {
+                return null;
+            }
+            mValueOutput = true;
+            return mValue;
+        }
+    }
+}

+ 34 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+/**
+ * Indicates that an ASN.1 data value being read could not be decoded using
+ * Basic Encoding Rules (BER).
+ */
+public class BerDataValueFormatException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public BerDataValueFormatException(String message) {
+        super(message);
+    }
+
+    public BerDataValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 34 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+/**
+ * Reader of ASN.1 Basic Encoding Rules (BER) data values.
+ *
+ * <p>BER data value reader returns data values, one by one, from a source. The interpretation of
+ * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
+ * the elements of a SEQUENCE value) is left to clients of the reader.
+ */
+public interface BerDataValueReader {
+
+    /**
+     * Returns the next data value or {@code null} if end of input has been reached.
+     *
+     * @throws BerDataValueFormatException if the value being read is malformed.
+     */
+    BerDataValue readDataValue() throws BerDataValueFormatException;
+}

+ 225 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java

@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.asn1.Asn1TagClass;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
+ */
+public abstract class BerEncoding {
+    private BerEncoding() {}
+
+    /**
+     * Constructed vs primitive flag in the first identifier byte.
+     */
+    public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
+
+    /**
+     * Tag class: UNIVERSAL
+     */
+    public static final int TAG_CLASS_UNIVERSAL = 0;
+
+    /**
+     * Tag class: APPLICATION
+     */
+    public static final int TAG_CLASS_APPLICATION = 1;
+
+    /**
+     * Tag class: CONTEXT SPECIFIC
+     */
+    public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
+
+    /**
+     * Tag class: PRIVATE
+     */
+    public static final int TAG_CLASS_PRIVATE = 3;
+
+    /**
+     * Tag number: 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;
+
+    /**
+     * Tag number: NULL
+     */
+    public static final int TAG_NUMBER_NULL = 0x05;
+
+    /**
+     * Tag number: OBJECT IDENTIFIER
+     */
+    public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
+
+    /**
+     * Tag number: SEQUENCE
+     */
+    public static final int TAG_NUMBER_SEQUENCE = 0x10;
+
+    /**
+     * Tag number: SET
+     */
+    public static final int TAG_NUMBER_SET = 0x11;
+
+    /**
+     * 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:
+                return TAG_NUMBER_INTEGER;
+            case OBJECT_IDENTIFIER:
+                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);
+        }
+    }
+
+    public static int getTagClass(Asn1TagClass tagClass) {
+        switch (tagClass) {
+            case APPLICATION:
+                return TAG_CLASS_APPLICATION;
+            case CONTEXT_SPECIFIC:
+                return TAG_CLASS_CONTEXT_SPECIFIC;
+            case PRIVATE:
+                return TAG_CLASS_PRIVATE;
+            case UNIVERSAL:
+                return TAG_CLASS_UNIVERSAL;
+            default:
+                throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
+        }
+    }
+
+    public static String tagClassToString(int typeClass) {
+        switch (typeClass) {
+            case TAG_CLASS_APPLICATION:
+                return "APPLICATION";
+            case TAG_CLASS_CONTEXT_SPECIFIC:
+                return "";
+            case TAG_CLASS_PRIVATE:
+                return "PRIVATE";
+            case TAG_CLASS_UNIVERSAL:
+                return "UNIVERSAL";
+            default:
+                throw new IllegalArgumentException("Unsupported type class: " + typeClass);
+        }
+    }
+
+    public static String tagClassAndNumberToString(int tagClass, int tagNumber) {
+        String classString = tagClassToString(tagClass);
+        String numberString = tagNumberToString(tagNumber);
+        return classString.isEmpty() ? numberString : classString + " " + numberString;
+    }
+
+
+    public static String tagNumberToString(int tagNumber) {
+        switch (tagNumber) {
+            case TAG_NUMBER_INTEGER:
+                return "INTEGER";
+            case TAG_NUMBER_OCTET_STRING:
+                return "OCTET STRING";
+            case TAG_NUMBER_BIT_STRING:
+                return "BIT STRING";
+            case TAG_NUMBER_NULL:
+                return "NULL";
+            case TAG_NUMBER_OBJECT_IDENTIFIER:
+                return "OBJECT IDENTIFIER";
+            case TAG_NUMBER_SEQUENCE:
+                return "SEQUENCE";
+            case TAG_NUMBER_SET:
+                return "SET";
+            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);
+        }
+    }
+
+    /**
+     * Returns {@code true} if the provided first identifier byte indicates that the data value uses
+     * constructed encoding for its contents, or {@code false} if the data value uses primitive
+     * encoding for its contents.
+     */
+    public static boolean isConstructed(byte firstIdentifierByte) {
+        return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
+    }
+
+    /**
+     * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
+     * constants.
+     */
+    public static int getTagClass(byte firstIdentifierByte) {
+        return (firstIdentifierByte & 0xff) >> 6;
+    }
+
+    public static byte setTagClass(byte firstIdentifierByte, int tagClass) {
+        return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6));
+    }
+
+    /**
+     * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
+     * constants.
+     */
+    public static int getTagNumber(byte firstIdentifierByte) {
+        return firstIdentifierByte & 0x1f;
+    }
+
+    public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) {
+        return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
+    }
+}

+ 208 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java

@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class ByteBufferBerDataValueReader implements BerDataValueReader {
+    private final ByteBuffer mBuf;
+
+    public ByteBufferBerDataValueReader(ByteBuffer buf) {
+        if (buf == null) {
+            throw new NullPointerException("buf == null");
+        }
+        mBuf = buf;
+    }
+
+    @Override
+    public BerDataValue readDataValue() throws BerDataValueFormatException {
+        int startPosition = mBuf.position();
+        if (!mBuf.hasRemaining()) {
+            return null;
+        }
+        byte firstIdentifierByte = mBuf.get();
+        int tagNumber = readTagNumber(firstIdentifierByte);
+        boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
+
+        if (!mBuf.hasRemaining()) {
+            throw new BerDataValueFormatException("Missing length");
+        }
+        int firstLengthByte = mBuf.get() & 0xff;
+        int contentsLength;
+        int contentsOffsetInTag;
+        if ((firstLengthByte & 0x80) == 0) {
+            // short form length
+            contentsLength = readShortFormLength(firstLengthByte);
+            contentsOffsetInTag = mBuf.position() - startPosition;
+            skipDefiniteLengthContents(contentsLength);
+        } else if (firstLengthByte != 0x80) {
+            // long form length
+            contentsLength = readLongFormLength(firstLengthByte);
+            contentsOffsetInTag = mBuf.position() - startPosition;
+            skipDefiniteLengthContents(contentsLength);
+        } else {
+            // indefinite length -- value ends with 0x00 0x00
+            contentsOffsetInTag = mBuf.position() - startPosition;
+            contentsLength =
+                    constructed
+                            ? skipConstructedIndefiniteLengthContents()
+                            : skipPrimitiveIndefiniteLengthContents();
+        }
+
+        // Create the encoded data value ByteBuffer
+        int endPosition = mBuf.position();
+        mBuf.position(startPosition);
+        int bufOriginalLimit = mBuf.limit();
+        mBuf.limit(endPosition);
+        ByteBuffer encoded = mBuf.slice();
+        mBuf.position(mBuf.limit());
+        mBuf.limit(bufOriginalLimit);
+
+        // Create the encoded contents ByteBuffer
+        encoded.position(contentsOffsetInTag);
+        encoded.limit(contentsOffsetInTag + contentsLength);
+        ByteBuffer encodedContents = encoded.slice();
+        encoded.clear();
+
+        return new BerDataValue(
+                encoded,
+                encodedContents,
+                BerEncoding.getTagClass(firstIdentifierByte),
+                constructed,
+                tagNumber);
+    }
+
+    private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
+        int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
+        if (tagNumber == 0x1f) {
+            // high-tag-number form, where the tag number follows this byte in base-128
+            // big-endian form, where each byte has the highest bit set, except for the last
+            // byte
+            return readHighTagNumber();
+        } else {
+            // low-tag-number form
+            return tagNumber;
+        }
+    }
+
+    private int readHighTagNumber() throws BerDataValueFormatException {
+        // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+        // byte
+        int b;
+        int result = 0;
+        do {
+            if (!mBuf.hasRemaining()) {
+                throw new BerDataValueFormatException("Truncated tag number");
+            }
+            b = mBuf.get();
+            if (result > Integer.MAX_VALUE >>> 7) {
+                throw new BerDataValueFormatException("Tag number too large");
+            }
+            result <<= 7;
+            result |= b & 0x7f;
+        } while ((b & 0x80) != 0);
+        return result;
+    }
+
+    private int readShortFormLength(int firstLengthByte) {
+        return firstLengthByte & 0x7f;
+    }
+
+    private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
+        // The low 7 bits of the first byte represent the number of bytes (following the first
+        // byte) in which the length is in big-endian base-256 form
+        int byteCount = firstLengthByte & 0x7f;
+        if (byteCount > 4) {
+            throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+        }
+        int result = 0;
+        for (int i = 0; i < byteCount; i++) {
+            if (!mBuf.hasRemaining()) {
+                throw new BerDataValueFormatException("Truncated length");
+            }
+            int b = mBuf.get();
+            if (result > Integer.MAX_VALUE >>> 8) {
+                throw new BerDataValueFormatException("Length too large");
+            }
+            result <<= 8;
+            result |= b & 0xff;
+        }
+        return result;
+    }
+
+    private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
+        if (mBuf.remaining() < contentsLength) {
+            throw new BerDataValueFormatException(
+                    "Truncated contents. Need: " + contentsLength + " bytes, available: "
+                            + mBuf.remaining());
+        }
+        mBuf.position(mBuf.position() + contentsLength);
+    }
+
+    private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
+        // Contents are terminated by 0x00 0x00
+        boolean prevZeroByte = false;
+        int bytesRead = 0;
+        while (true) {
+            if (!mBuf.hasRemaining()) {
+                throw new BerDataValueFormatException(
+                        "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+
+            }
+            int b = mBuf.get();
+            bytesRead++;
+            if (bytesRead < 0) {
+                throw new BerDataValueFormatException("Indefinite-length contents too long");
+            }
+            if (b == 0) {
+                if (prevZeroByte) {
+                    // End of contents reached -- we've read the value and its terminator 0x00 0x00
+                    return bytesRead - 2;
+                }
+                prevZeroByte = true;
+            } else {
+                prevZeroByte = false;
+            }
+        }
+    }
+
+    private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException {
+        // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
+        // can contain data values which are themselves indefinite length encoded. As a result, we
+        // must parse the direct children of this data value to correctly skip over the contents of
+        // this data value.
+        int startPos = mBuf.position();
+        while (mBuf.hasRemaining()) {
+            // Check whether the 0x00 0x00 terminator is at current position
+            if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) {
+                int contentsLength = mBuf.position() - startPos;
+                mBuf.position(mBuf.position() + 2);
+                return contentsLength;
+            }
+            // No luck. This must be a BER-encoded data value -- skip over it by parsing it
+            readDataValue();
+        }
+
+        throw new BerDataValueFormatException(
+                "Truncated indefinite-length contents: "
+                        + (mBuf.position() - startPos) + " bytes read");
+    }
+}

+ 313 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java

@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1.ber;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class InputStreamBerDataValueReader implements BerDataValueReader {
+    private final InputStream mIn;
+
+    public InputStreamBerDataValueReader(InputStream in) {
+        if (in == null) {
+            throw new NullPointerException("in == null");
+        }
+        mIn = in;
+    }
+
+    @Override
+    public BerDataValue readDataValue() throws BerDataValueFormatException {
+        return readDataValue(mIn);
+    }
+
+    /**
+     * Returns the next data value or {@code null} if end of input has been reached.
+     *
+     * @throws BerDataValueFormatException if the value being read is malformed.
+     */
+    @SuppressWarnings("resource")
+    private static BerDataValue readDataValue(InputStream input)
+            throws BerDataValueFormatException {
+        RecordingInputStream in = new RecordingInputStream(input);
+
+        try {
+            int firstIdentifierByte = in.read();
+            if (firstIdentifierByte == -1) {
+                // End of input
+                return null;
+            }
+            int tagNumber = readTagNumber(in, firstIdentifierByte);
+
+            int firstLengthByte = in.read();
+            if (firstLengthByte == -1) {
+                throw new BerDataValueFormatException("Missing length");
+            }
+
+            boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
+            int contentsLength;
+            int contentsOffsetInDataValue;
+            if ((firstLengthByte & 0x80) == 0) {
+                // short form length
+                contentsLength = readShortFormLength(firstLengthByte);
+                contentsOffsetInDataValue = in.getReadByteCount();
+                skipDefiniteLengthContents(in, contentsLength);
+            } else if ((firstLengthByte & 0xff) != 0x80) {
+                // long form length
+                contentsLength = readLongFormLength(in, firstLengthByte);
+                contentsOffsetInDataValue = in.getReadByteCount();
+                skipDefiniteLengthContents(in, contentsLength);
+            } else {
+                // indefinite length
+                contentsOffsetInDataValue = in.getReadByteCount();
+                contentsLength =
+                        constructed
+                                ? skipConstructedIndefiniteLengthContents(in)
+                                : skipPrimitiveIndefiniteLengthContents(in);
+            }
+
+            byte[] encoded = in.getReadBytes();
+            ByteBuffer encodedContents =
+                    ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
+            return new BerDataValue(
+                    ByteBuffer.wrap(encoded),
+                    encodedContents,
+                    BerEncoding.getTagClass((byte) firstIdentifierByte),
+                    constructed,
+                    tagNumber);
+        } catch (IOException e) {
+            throw new BerDataValueFormatException("Failed to read data value", e);
+        }
+    }
+
+    private static int readTagNumber(InputStream in, int firstIdentifierByte)
+            throws IOException, BerDataValueFormatException {
+        int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
+        if (tagNumber == 0x1f) {
+            // high-tag-number form
+            return readHighTagNumber(in);
+        } else {
+            // low-tag-number form
+            return tagNumber;
+        }
+    }
+
+    private static int readHighTagNumber(InputStream in)
+            throws IOException, BerDataValueFormatException {
+        // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+        // byte where the highest bit is not set
+        int b;
+        int result = 0;
+        do {
+            b = in.read();
+            if (b == -1) {
+                throw new BerDataValueFormatException("Truncated tag number");
+            }
+            if (result > Integer.MAX_VALUE >>> 7) {
+                throw new BerDataValueFormatException("Tag number too large");
+            }
+            result <<= 7;
+            result |= b & 0x7f;
+        } while ((b & 0x80) != 0);
+        return result;
+    }
+
+    private static int readShortFormLength(int firstLengthByte) {
+        return firstLengthByte & 0x7f;
+    }
+
+    private static int readLongFormLength(InputStream in, int firstLengthByte)
+            throws IOException, BerDataValueFormatException {
+        // The low 7 bits of the first byte represent the number of bytes (following the first
+        // byte) in which the length is in big-endian base-256 form
+        int byteCount = firstLengthByte & 0x7f;
+        if (byteCount > 4) {
+            throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+        }
+        int result = 0;
+        for (int i = 0; i < byteCount; i++) {
+            int b = in.read();
+            if (b == -1) {
+                throw new BerDataValueFormatException("Truncated length");
+            }
+            if (result > Integer.MAX_VALUE >>> 8) {
+                throw new BerDataValueFormatException("Length too large");
+            }
+            result <<= 8;
+            result |= b & 0xff;
+        }
+        return result;
+    }
+
+    private static void skipDefiniteLengthContents(InputStream in, int len)
+            throws IOException, BerDataValueFormatException {
+        long bytesRead = 0;
+        while (len > 0) {
+            int skipped = (int) in.skip(len);
+            if (skipped <= 0) {
+                throw new BerDataValueFormatException(
+                        "Truncated definite-length contents: " + bytesRead + " bytes read"
+                                + ", " + len + " missing");
+            }
+            len -= skipped;
+            bytesRead += skipped;
+        }
+    }
+
+    private static int skipPrimitiveIndefiniteLengthContents(InputStream in)
+            throws IOException, BerDataValueFormatException {
+        // Contents are terminated by 0x00 0x00
+        boolean prevZeroByte = false;
+        int bytesRead = 0;
+        while (true) {
+            int b = in.read();
+            if (b == -1) {
+                throw new BerDataValueFormatException(
+                        "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+            }
+            bytesRead++;
+            if (bytesRead < 0) {
+                throw new BerDataValueFormatException("Indefinite-length contents too long");
+            }
+            if (b == 0) {
+                if (prevZeroByte) {
+                    // End of contents reached -- we've read the value and its terminator 0x00 0x00
+                    return bytesRead - 2;
+                }
+                prevZeroByte = true;
+                continue;
+            } else {
+                prevZeroByte = false;
+            }
+        }
+    }
+
+    private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in)
+            throws BerDataValueFormatException {
+        // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
+        // can contain data values which are indefinite length encoded as well. As a result, we
+        // must parse the direct children of this data value to correctly skip over the contents of
+        // this data value.
+        int readByteCountBefore = in.getReadByteCount();
+        while (true) {
+            // We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
+            // Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
+            // then check below to see whether it's 0x00 0x00.
+            BerDataValue dataValue = readDataValue(in);
+            if (dataValue == null) {
+                throw new BerDataValueFormatException(
+                        "Truncated indefinite-length contents: "
+                                + (in.getReadByteCount() - readByteCountBefore) + " bytes read");
+            }
+            if (in.getReadByteCount() <= 0) {
+                throw new BerDataValueFormatException("Indefinite-length contents too long");
+            }
+            ByteBuffer encoded = dataValue.getEncoded();
+            if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
+                // 0x00 0x00 encountered
+                return in.getReadByteCount() - readByteCountBefore - 2;
+            }
+        }
+    }
+
+    private static class RecordingInputStream extends InputStream {
+        private final InputStream mIn;
+        private final ByteArrayOutputStream mBuf;
+
+        private RecordingInputStream(InputStream in) {
+            mIn = in;
+            mBuf = new ByteArrayOutputStream();
+        }
+
+        public byte[] getReadBytes() {
+            return mBuf.toByteArray();
+        }
+
+        public int getReadByteCount() {
+            return mBuf.size();
+        }
+
+        @Override
+        public int read() throws IOException {
+            int b = mIn.read();
+            if (b != -1) {
+                mBuf.write(b);
+            }
+            return b;
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            int len = mIn.read(b);
+            if (len > 0) {
+                mBuf.write(b, 0, len);
+            }
+            return len;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            len = mIn.read(b, off, len);
+            if (len > 0) {
+                mBuf.write(b, off, len);
+            }
+            return len;
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            if (n <= 0) {
+                return mIn.skip(n);
+            }
+
+            byte[] buf = new byte[4096];
+            int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
+            if (len > 0) {
+                mBuf.write(buf, 0, len);
+            }
+            return (len < 0) ? 0 : len;
+        }
+
+        @Override
+        public int available() throws IOException {
+            return super.available();
+        }
+
+        @Override
+        public void close() throws IOException {
+            super.close();
+        }
+
+        @Override
+        public synchronized void mark(int readlimit) {}
+
+        @Override
+        public synchronized void reset() throws IOException {
+            throw new IOException("mark/reset not supported");
+        }
+
+        @Override
+        public boolean markSupported() {
+            return false;
+        }
+    }
+}

+ 363 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java

@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2016 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.jar;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+
+/**
+ * JAR manifest and signature file parser.
+ *
+ * <p>These files consist of a main section followed by individual sections. Individual sections
+ * are named, their names referring to JAR entries.
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public class ManifestParser {
+
+    private final byte[] mManifest;
+    private int mOffset;
+    private int mEndOffset;
+
+    private byte[] mBufferedLine;
+
+    /**
+     * Constructs a new {@code ManifestParser} with the provided input.
+     */
+    public ManifestParser(byte[] data) {
+        this(data, 0, data.length);
+    }
+
+    /**
+     * Constructs a new {@code ManifestParser} with the provided input.
+     */
+    public ManifestParser(byte[] data, int offset, int length) {
+        mManifest = data;
+        mOffset = offset;
+        mEndOffset = offset + length;
+    }
+
+    /**
+     * Returns the remaining sections of this file.
+     */
+    public List<Section> readAllSections() {
+        List<Section> sections = new ArrayList<>();
+        Section section;
+        while ((section = readSection()) != null) {
+            sections.add(section);
+        }
+        return sections;
+    }
+
+    /**
+     * Returns the next section from this file or {@code null} if end of file has been reached.
+     */
+    public Section readSection() {
+        // Locate the first non-empty line
+        int sectionStartOffset;
+        String attr;
+        do {
+            sectionStartOffset = mOffset;
+            attr = readAttribute();
+            if (attr == null) {
+                return null;
+            }
+        } while (attr.length() == 0);
+        List<Attribute> attrs = new ArrayList<>();
+        attrs.add(parseAttr(attr));
+
+        // Read attributes until end of section reached
+        while (true) {
+            attr = readAttribute();
+            if ((attr == null) || (attr.length() == 0)) {
+                // End of section
+                break;
+            }
+            attrs.add(parseAttr(attr));
+        }
+
+        int sectionEndOffset = mOffset;
+        int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
+
+        return new Section(sectionStartOffset, sectionSizeBytes, attrs);
+    }
+
+    private static Attribute parseAttr(String attr) {
+        // Name is separated from value by a semicolon followed by a single SPACE character.
+        // This permits trailing spaces in names and leading and trailing spaces in values.
+        // Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual
+        // spaces to be able to parse such obfuscated APKs.
+        int delimiterIndex = attr.indexOf(": ");
+        if (delimiterIndex == -1) {
+            return new Attribute(attr, "");
+        } else {
+            return new Attribute(
+                    attr.substring(0, delimiterIndex),
+                    attr.substring(delimiterIndex + ": ".length()));
+        }
+    }
+
+    /**
+     * Returns the next attribute or empty {@code String} if end of section has been reached or
+     * {@code null} if end of input has been reached.
+     */
+    private String readAttribute() {
+        byte[] bytes = readAttributeBytes();
+        if (bytes == null) {
+            return null;
+        } else if (bytes.length == 0) {
+            return "";
+        } else {
+            return new String(bytes, StandardCharsets.UTF_8);
+        }
+    }
+
+    /**
+     * Returns the next attribute or empty array if end of section has been reached or {@code null}
+     * if end of input has been reached.
+     */
+    private byte[] readAttributeBytes() {
+        // Check whether end of section was reached during previous invocation
+        if ((mBufferedLine != null) && (mBufferedLine.length == 0)) {
+            mBufferedLine = null;
+            return EMPTY_BYTE_ARRAY;
+        }
+
+        // Read the next line
+        byte[] line = readLine();
+        if (line == null) {
+            // End of input
+            if (mBufferedLine != null) {
+                byte[] result = mBufferedLine;
+                mBufferedLine = null;
+                return result;
+            }
+            return null;
+        }
+
+        // Consume the read line
+        if (line.length == 0) {
+            // End of section
+            if (mBufferedLine != null) {
+                byte[] result = mBufferedLine;
+                mBufferedLine = EMPTY_BYTE_ARRAY;
+                return result;
+            }
+            return EMPTY_BYTE_ARRAY;
+        }
+        byte[] attrLine;
+        if (mBufferedLine == null) {
+            attrLine = line;
+        } else {
+            if ((line.length == 0) || (line[0] != ' ')) {
+                // The most common case: buffered line is a full attribute
+                byte[] result = mBufferedLine;
+                mBufferedLine = line;
+                return result;
+            }
+            attrLine = mBufferedLine;
+            mBufferedLine = null;
+            attrLine = concat(attrLine, line, 1, line.length - 1);
+        }
+
+        // Everything's buffered in attrLine now. mBufferedLine is null
+
+        // Read more lines
+        while (true) {
+            line = readLine();
+            if (line == null) {
+                // End of input
+                return attrLine;
+            } else if (line.length == 0) {
+                // End of section
+                mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time
+                return attrLine;
+            }
+            if (line[0] == ' ') {
+                // Continuation line
+                attrLine = concat(attrLine, line, 1, line.length - 1);
+            } else {
+                // Next attribute
+                mBufferedLine = line;
+                return attrLine;
+            }
+        }
+    }
+
+    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+    private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) {
+        byte[] result = new byte[arr1.length + length2];
+        System.arraycopy(arr1, 0, result, 0, arr1.length);
+        System.arraycopy(arr2, offset2, result, arr1.length, length2);
+        return result;
+    }
+
+    /**
+     * Returns the next line (without line delimiter characters) or {@code null} if end of input has
+     * been reached.
+     */
+    private byte[] readLine() {
+        if (mOffset >= mEndOffset) {
+            return null;
+        }
+        int startOffset = mOffset;
+        int newlineStartOffset = -1;
+        int newlineEndOffset = -1;
+        for (int i = startOffset; i < mEndOffset; i++) {
+            byte b = mManifest[i];
+            if (b == '\r') {
+                newlineStartOffset = i;
+                int nextIndex = i + 1;
+                if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
+                    newlineEndOffset = nextIndex + 1;
+                    break;
+                }
+                newlineEndOffset = nextIndex;
+                break;
+            } else if (b == '\n') {
+                newlineStartOffset = i;
+                newlineEndOffset = i + 1;
+                break;
+            }
+        }
+        if (newlineStartOffset == -1) {
+            newlineStartOffset = mEndOffset;
+            newlineEndOffset = mEndOffset;
+        }
+        mOffset = newlineEndOffset;
+
+        if (newlineStartOffset == startOffset) {
+            return EMPTY_BYTE_ARRAY;
+        }
+        return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset);
+    }
+
+
+    /**
+     * Attribute.
+     */
+    public static class Attribute {
+        private final String mName;
+        private final String mValue;
+
+        /**
+         * Constructs a new {@code Attribute} with the provided name and value.
+         */
+        public Attribute(String name, String value) {
+            mName = name;
+            mValue = value;
+        }
+
+        /**
+         * Returns this attribute's name.
+         */
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Returns this attribute's value.
+         */
+        public String getValue() {
+            return mValue;
+        }
+    }
+
+    /**
+     * Section.
+     */
+    public static class Section {
+        private final int mStartOffset;
+        private final int mSizeBytes;
+        private final String mName;
+        private final List<Attribute> mAttributes;
+
+        /**
+         * Constructs a new {@code Section}.
+         *
+         * @param startOffset start offset (in bytes) of the section in the input file
+         * @param sizeBytes size (in bytes) of the section in the input file
+         * @param attrs attributes contained in the section
+         */
+        public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
+            mStartOffset = startOffset;
+            mSizeBytes = sizeBytes;
+            String sectionName = null;
+            if (!attrs.isEmpty()) {
+                Attribute firstAttr = attrs.get(0);
+                if ("Name".equalsIgnoreCase(firstAttr.getName())) {
+                    sectionName = firstAttr.getValue();
+                }
+            }
+            mName = sectionName;
+            mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Returns the offset (in bytes) at which this section starts in the input.
+         */
+        public int getStartOffset() {
+            return mStartOffset;
+        }
+
+        /**
+         * Returns the size (in bytes) of this section in the input.
+         */
+        public int getSizeBytes() {
+            return mSizeBytes;
+        }
+
+        /**
+         * Returns this section's attributes, in the order in which they appear in the input.
+         */
+        public List<Attribute> getAttributes() {
+            return mAttributes;
+        }
+
+        /**
+         * Returns the value of the specified attribute in this section or {@code null} if this
+         * section does not contain a matching attribute.
+         */
+        public String getAttributeValue(Attributes.Name name) {
+            return getAttributeValue(name.toString());
+        }
+
+        /**
+         * Returns the value of the specified attribute in this section or {@code null} if this
+         * section does not contain a matching attribute.
+         *
+         * @param name name of the attribute. Attribute names are case-insensitive.
+         */
+        public String getAttributeValue(String name) {
+            for (Attribute attr : mAttributes) {
+                if (attr.getName().equalsIgnoreCase(name)) {
+                    return attr.getValue();
+                }
+            }
+            return null;
+        }
+    }
+}

+ 127 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 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.jar;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+
+/**
+ * Producer of {@code META-INF/MANIFEST.MF} file.
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public abstract class ManifestWriter {
+
+    private static final byte[] CRLF = new byte[] {'\r', '\n'};
+    private static final int MAX_LINE_LENGTH = 70;
+
+    private ManifestWriter() {}
+
+    public static void writeMainSection(OutputStream out, Attributes attributes)
+            throws IOException {
+
+        // Main section must start with the Manifest-Version attribute.
+        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
+        String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
+        if (manifestVersion == null) {
+            throw new IllegalArgumentException(
+                    "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
+        }
+        writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
+
+        if (attributes.size() > 1) {
+            SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
+            namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
+            writeAttributes(out, namedAttributes);
+        }
+        writeSectionDelimiter(out);
+    }
+
+    public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
+            throws IOException {
+        writeAttribute(out, "Name", name);
+
+        if (!attributes.isEmpty()) {
+            writeAttributes(out, getAttributesSortedByName(attributes));
+        }
+        writeSectionDelimiter(out);
+    }
+
+    static void writeSectionDelimiter(OutputStream out) throws IOException {
+        out.write(CRLF);
+    }
+
+    static void writeAttribute(OutputStream  out, Attributes.Name name, String value)
+            throws IOException {
+        writeAttribute(out, name.toString(), value);
+    }
+
+    private static void writeAttribute(OutputStream  out, String name, String value)
+            throws IOException {
+        writeLine(out, name + ": " + value);
+    }
+
+    private static void writeLine(OutputStream  out, String line) throws IOException {
+        byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
+        int offset = 0;
+        int remaining = lineBytes.length;
+        boolean firstLine = true;
+        while (remaining > 0) {
+            int chunkLength;
+            if (firstLine) {
+                // First line
+                chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
+            } else {
+                // Continuation line
+                out.write(CRLF);
+                out.write(' ');
+                chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
+            }
+            out.write(lineBytes, offset, chunkLength);
+            offset += chunkLength;
+            remaining -= chunkLength;
+            firstLine = false;
+        }
+        out.write(CRLF);
+    }
+
+    static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
+        Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
+        SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
+        for (Map.Entry<Object, Object> attribute : attributesEntries) {
+            String attrName = attribute.getKey().toString();
+            String attrValue = attribute.getValue().toString();
+            namedAttributes.put(attrName, attrValue);
+        }
+        return namedAttributes;
+    }
+
+    static void writeAttributes(
+            OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
+        for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
+            String attrName = attribute.getKey();
+            String attrValue = attribute.getValue();
+            writeAttribute(out, attrName, attrValue);
+        }
+    }
+}

+ 61 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 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.jar;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.SortedMap;
+import java.util.jar.Attributes;
+
+/**
+ * Producer of JAR signature file ({@code *.SF}).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public abstract class SignatureFileWriter {
+    private SignatureFileWriter() {}
+
+    public static void writeMainSection(OutputStream out, Attributes attributes)
+            throws IOException {
+
+        // Main section must start with the Signature-Version attribute.
+        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
+        String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
+        if (signatureVersion == null) {
+            throw new IllegalArgumentException(
+                    "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
+        }
+        ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
+
+        if (attributes.size() > 1) {
+            SortedMap<String, String> namedAttributes =
+                    ManifestWriter.getAttributesSortedByName(attributes);
+            namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
+            ManifestWriter.writeAttributes(out, namedAttributes);
+        }
+        writeSectionDelimiter(out);
+    }
+
+    public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
+            throws IOException {
+        ManifestWriter.writeIndividualSection(out, name, attributes);
+    }
+
+    public static void writeSectionDelimiter(OutputStream out) throws IOException {
+        ManifestWriter.writeSectionDelimiter(out);
+    }
+}

+ 463 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java

@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2020 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.oid;
+
+import com.android.apksig.internal.util.InclusiveIntRange;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OidConstants {
+    public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
+    public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
+    public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
+    public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
+    public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
+    public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
+
+    public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
+    public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
+    public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
+    public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
+    public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
+    public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
+    public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
+
+    public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
+    public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
+    public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
+    public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
+    public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
+    public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
+
+    public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
+    public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
+    public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
+    public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
+    public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
+    public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
+
+    public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
+            new HashMap<>();
+    static {
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_RSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_RSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_RSA,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_RSA,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.from(21));
+
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_DSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.from(9));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_DSA,
+                InclusiveIntRange.from(22));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_DSA,
+                InclusiveIntRange.from(22));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.from(21));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+    }
+
+    public static void addSupportedSigAlg(
+            String digestAlgorithmOid,
+            String signatureAlgorithmOid,
+            InclusiveIntRange... supportedApiLevels) {
+        SUPPORTED_SIG_ALG_OIDS.put(
+                digestAlgorithmOid + "with" + signatureAlgorithmOid,
+                Arrays.asList(supportedApiLevels));
+    }
+
+    public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
+            String digestAlgorithmOid,
+            String signatureAlgorithmOid) {
+        List<InclusiveIntRange> result =
+                SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
+        return (result != null) ? result : Collections.emptyList();
+    }
+
+    public static class OidToUserFriendlyNameMapper {
+        private OidToUserFriendlyNameMapper() {}
+
+        private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
+        static {
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
+
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
+
+
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
+            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");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
+        }
+
+        public static String getUserFriendlyNameForOid(String oid) {
+            return OID_TO_USER_FRIENDLY_NAME.get(oid);
+        }
+    }
+
+    public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
+    static {
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
+    }
+
+    public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
+    static {
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
+
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
+
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
+    }
+
+    private OidConstants() {}
+}

+ 173 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java

@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
+import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
+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 static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
+
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+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;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class AlgorithmIdentifier {
+
+    @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+    public String algorithm;
+
+    @Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
+    public Asn1OpaqueObject parameters;
+
+    public AlgorithmIdentifier() {}
+
+    public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
+        this.algorithm = algorithmOid;
+        this.parameters = parameters;
+    }
+
+    /**
+     * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
+     * algorithm.
+     */
+    public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
+            DigestAlgorithm digestAlgorithm) {
+        switch (digestAlgorithm) {
+            case SHA1:
+                return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
+            case SHA256:
+                return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
+        }
+        throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
+    }
+
+    /**
+     * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
+     * when signing with the specified key and digest algorithm.
+     */
+    public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
+            PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
+            throws InvalidKeyException {
+        String keyAlgorithm = publicKey.getAlgorithm();
+        String jcaDigestPrefixForSigAlg;
+        switch (digestAlgorithm) {
+            case SHA1:
+                jcaDigestPrefixForSigAlg = "SHA1";
+                break;
+            case SHA256:
+                jcaDigestPrefixForSigAlg = "SHA256";
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected digest algorithm: " + digestAlgorithm);
+        }
+        if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
+            return Pair.of(
+                    jcaDigestPrefixForSigAlg + "withRSA",
+                    new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            AlgorithmIdentifier sigAlgId;
+            switch (digestAlgorithm) {
+                case SHA1:
+                    sigAlgId =
+                            new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
+                    break;
+                case SHA256:
+                    // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
+                    // 21 and higher. However, there are two ways to specify their SignedData
+                    // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
+                    // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
+                    // the former.
+                    sigAlgId =
+                            new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unexpected digest algorithm: " + digestAlgorithm);
+            }
+            String signingAlgorithmName =
+                    jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
+            return Pair.of(signingAlgorithmName, sigAlgId);
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            return Pair.of(
+                    jcaDigestPrefixForSigAlg + "withECDSA",
+                    new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    public static String getJcaSignatureAlgorithm(
+            String digestAlgorithmOid,
+            String signatureAlgorithmOid) throws SignatureException {
+        // First check whether the signature algorithm OID alone is sufficient
+        String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
+        if (result != null) {
+            return result;
+        }
+
+        // Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
+        // with signature algorithm OID.
+        String suffix;
+        if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
+            suffix = "RSA";
+        } else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
+            suffix = "DSA";
+        } else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
+            suffix = "ECDSA";
+        } else {
+            throw new SignatureException(
+                    "Unsupported JCA Signature algorithm"
+                            + " . Digest algorithm: " + digestAlgorithmOid
+                            + ", signature algorithm: " + signatureAlgorithmOid);
+        }
+        String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
+        // Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
+        // SHA algorithms.
+        if (jcaDigestAlg.startsWith("SHA-")) {
+            jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
+        }
+        return jcaDigestAlg + "with" + suffix;
+    }
+
+    public static String getJcaDigestAlgorithm(String oid)
+            throws SignatureException {
+        String result = OID_TO_JCA_DIGEST_ALG.get(oid);
+        if (result == null) {
+            throw new SignatureException("Unsupported digest algorithm: " + oid);
+        }
+        return result;
+    }
+}

+ 36 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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;
+import java.util.List;
+
+/**
+ * PKCS #7 {@code Attribute} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Attribute {
+
+    @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+    public String attrType;
+
+    @Asn1Field(index = 1, type = Asn1Type.SET_OF)
+    public List<Asn1OpaqueObject> attrValues;
+}

+ 36 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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;
+import com.android.apksig.internal.asn1.Asn1Tagging;
+
+/**
+ * PKCS #7 {@code ContentInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class ContentInfo {
+
+    @Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
+    public String contentType;
+
+    @Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
+    public Asn1OpaqueObject content;
+}

+ 46 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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 java.nio.ByteBuffer;
+
+/**
+ * PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class EncapsulatedContentInfo {
+
+    @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+    public String contentType;
+
+    @Asn1Field(
+            index = 1,
+            type = Asn1Type.OCTET_STRING,
+            tagging = Asn1Tagging.EXPLICIT, tagNumber = 0,
+            optional = true)
+    public ByteBuffer content;
+
+    public EncapsulatedContentInfo() {}
+
+    public EncapsulatedContentInfo(String contentTypeOid) {
+        contentType = contentTypeOid;
+    }
+}

+ 43 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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;
+import java.math.BigInteger;
+
+/**
+ * PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class IssuerAndSerialNumber {
+
+    @Asn1Field(index = 0, type = Asn1Type.ANY)
+    public Asn1OpaqueObject issuer;
+
+    @Asn1Field(index = 1, type = Asn1Type.INTEGER)
+    public BigInteger certificateSerialNumber;
+
+    public IssuerAndSerialNumber() {}
+
+    public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
+        this.issuer = issuer;
+        this.certificateSerialNumber = certificateSerialNumber;
+    }
+}

+ 29 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+/**
+ * Assorted PKCS #7 constants from RFC 5652.
+ */
+public abstract class Pkcs7Constants {
+    private Pkcs7Constants() {}
+
+    public static final String OID_DATA = "1.2.840.113549.1.7.1";
+    public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
+    public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
+    public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
+}

+ 32 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+/**
+ * Indicates that an error was encountered while decoding a PKCS #7 structure.
+ */
+public class Pkcs7DecodingException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public Pkcs7DecodingException(String message) {
+        super(message);
+    }
+
+    public Pkcs7DecodingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 58 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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;
+import com.android.apksig.internal.asn1.Asn1Tagging;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * PKCS #7 {@code SignedData} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class SignedData {
+
+    @Asn1Field(index = 0, type = Asn1Type.INTEGER)
+    public int version;
+
+    @Asn1Field(index = 1, type = Asn1Type.SET_OF)
+    public List<AlgorithmIdentifier> digestAlgorithms;
+
+    @Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
+    public EncapsulatedContentInfo encapContentInfo;
+
+    @Asn1Field(
+            index = 3,
+            type = Asn1Type.SET_OF,
+            tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
+            optional = true)
+    public List<Asn1OpaqueObject> certificates;
+
+    @Asn1Field(
+            index = 4,
+            type = Asn1Type.SET_OF,
+            tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
+            optional = true)
+    public List<ByteBuffer> crls;
+
+    @Asn1Field(index = 5, type = Asn1Type.SET_OF)
+    public List<SignerInfo> signerInfos;
+}

+ 42 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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 java.nio.ByteBuffer;
+
+/**
+ * PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.CHOICE)
+public class SignerIdentifier {
+
+    @Asn1Field(type = Asn1Type.SEQUENCE)
+    public IssuerAndSerialNumber issuerAndSerialNumber;
+
+    @Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
+    public ByteBuffer subjectKeyIdentifier;
+
+    public SignerIdentifier() {}
+
+    public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
+        this.issuerAndSerialNumber = issuerAndSerialNumber;
+    }
+}

+ 61 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+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;
+import com.android.apksig.internal.asn1.Asn1Tagging;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * PKCS #7 {@code SignerInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class SignerInfo {
+
+    @Asn1Field(index = 0, type = Asn1Type.INTEGER)
+    public int version;
+
+    @Asn1Field(index = 1, type = Asn1Type.CHOICE)
+    public SignerIdentifier sid;
+
+    @Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
+    public AlgorithmIdentifier digestAlgorithm;
+
+    @Asn1Field(
+            index = 3,
+            type = Asn1Type.SET_OF,
+            tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
+            optional = true)
+    public Asn1OpaqueObject signedAttrs;
+
+    @Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
+    public AlgorithmIdentifier signatureAlgorithm;
+
+    @Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
+    public ByteBuffer signature;
+
+    @Asn1Field(
+            index = 6,
+            type = Asn1Type.SET_OF,
+            tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
+            optional = true)
+    public List<Attribute> unsignedAttrs;
+}

+ 74 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 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;
+
+/**
+ * Android SDK version / API Level constants.
+ */
+public abstract class AndroidSdkVersion {
+
+    /** Hidden constructor to prevent instantiation. */
+    private AndroidSdkVersion() {}
+
+    /** Android 1.0 */
+    public static final int INITIAL_RELEASE = 1;
+
+    /** Android 2.3. */
+    public static final int GINGERBREAD = 9;
+
+    /** Android 3.0 */
+    public static final int HONEYCOMB = 11;
+
+    /** Android 4.3. The revenge of the beans. */
+    public static final int JELLY_BEAN_MR2 = 18;
+
+    /** Android 4.4. KitKat, another tasty treat. */
+    public static final int KITKAT = 19;
+
+    /** Android 5.0. A flat one with beautiful shadows. But still tasty. */
+    public static final int LOLLIPOP = 21;
+
+    /** Android 6.0. M is for Marshmallow! */
+    public static final int M = 23;
+
+    /** Android 7.0. N is for Nougat. */
+    public static final int N = 24;
+
+    /** Android O. */
+    public static final int O = 26;
+
+    /** Android P. */
+    public static final int P = 28;
+
+    /** Android Q. */
+    public static final int Q = 29;
+
+    /** Android R. */
+    public static final int R = 30;
+
+    /** Android S. */
+    public static final int S = 31;
+
+    /** Android Sv2. */
+    public static final int Sv2 = 32;
+
+    /** Android Tiramisu. */
+    public static final int T = 33;
+
+    /** Android Upside Down Cake. */
+    public static final int U = 34;
+}

+ 240 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java

@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.ReadableDataSink;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Growable byte array which can be appended to via {@link DataSink} interface and read from via
+ * {@link DataSource} interface.
+ */
+public class ByteArrayDataSink implements ReadableDataSink {
+
+    private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+    private byte[] mArray;
+    private int mSize;
+
+    public ByteArrayDataSink() {
+        this(65536);
+    }
+
+    public ByteArrayDataSink(int initialCapacity) {
+        if (initialCapacity < 0) {
+            throw new IllegalArgumentException("initial capacity: " + initialCapacity);
+        }
+        mArray = new byte[initialCapacity];
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        if (offset < 0) {
+            // Must perform this check because System.arraycopy below doesn't perform it when
+            // length == 0
+            throw new IndexOutOfBoundsException("offset: " + offset);
+        }
+        if (offset > buf.length) {
+            // Must perform this check because System.arraycopy below doesn't perform it when
+            // length == 0
+            throw new IndexOutOfBoundsException(
+                    "offset: " + offset + ", buf.length: " + buf.length);
+        }
+        if (length == 0) {
+            return;
+        }
+
+        ensureAvailable(length);
+        System.arraycopy(buf, offset, mArray, mSize, length);
+        mSize += length;
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        if (!buf.hasRemaining()) {
+            return;
+        }
+
+        if (buf.hasArray()) {
+            consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+            buf.position(buf.limit());
+            return;
+        }
+
+        ensureAvailable(buf.remaining());
+        byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
+        while (buf.hasRemaining()) {
+            int chunkSize = Math.min(buf.remaining(), tmp.length);
+            buf.get(tmp, 0, chunkSize);
+            System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
+            mSize += chunkSize;
+        }
+    }
+
+    private void ensureAvailable(int minAvailable) throws IOException {
+        if (minAvailable <= 0) {
+            return;
+        }
+
+        long minCapacity = ((long) mSize) + minAvailable;
+        if (minCapacity <= mArray.length) {
+            return;
+        }
+        if (minCapacity > Integer.MAX_VALUE) {
+            throw new IOException(
+                    "Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
+        }
+        int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
+        int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
+        mArray = Arrays.copyOf(mArray, newSize);
+    }
+
+    @Override
+    public long size() {
+        return mSize;
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer(long offset, int size) {
+        checkChunkValid(offset, size);
+
+        // checkChunkValid ensures that it's OK to cast offset to int.
+        return ByteBuffer.wrap(mArray, (int) offset, size).slice();
+    }
+
+    @Override
+    public void feed(long offset, long size, DataSink sink) throws IOException {
+        checkChunkValid(offset, size);
+
+        // checkChunkValid ensures that it's OK to cast offset and size to int.
+        sink.consume(mArray, (int) offset, (int) size);
+    }
+
+    @Override
+    public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+        checkChunkValid(offset, size);
+
+        // checkChunkValid ensures that it's OK to cast offset to int.
+        dest.put(mArray, (int) offset, size);
+    }
+
+    private void checkChunkValid(long offset, long size) {
+        if (offset < 0) {
+            throw new IndexOutOfBoundsException("offset: " + offset);
+        }
+        if (size < 0) {
+            throw new IndexOutOfBoundsException("size: " + size);
+        }
+        if (offset > mSize) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") > source size (" + mSize + ")");
+        }
+        long endOffset = offset + size;
+        if (endOffset < offset) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") + size (" + size + ") overflow");
+        }
+        if (endOffset > mSize) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
+        }
+    }
+
+    @Override
+    public DataSource slice(long offset, long size) {
+        checkChunkValid(offset, size);
+        // checkChunkValid ensures that it's OK to cast offset and size to int.
+        return new SliceDataSource((int) offset, (int) size);
+    }
+
+    /**
+     * Slice of the growable byte array. The slice's offset and size in the array are fixed.
+     */
+    private class SliceDataSource implements DataSource {
+        private final int mSliceOffset;
+        private final int mSliceSize;
+
+        private SliceDataSource(int offset, int size) {
+            mSliceOffset = offset;
+            mSliceSize = size;
+        }
+
+        @Override
+        public long size() {
+            return mSliceSize;
+        }
+
+        @Override
+        public void feed(long offset, long size, DataSink sink) throws IOException {
+            checkChunkValid(offset, size);
+            // checkChunkValid combined with the way instances of this class are constructed ensures
+            // that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
+            sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
+        }
+
+        @Override
+        public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+            checkChunkValid(offset, size);
+            // checkChunkValid combined with the way instances of this class are constructed ensures
+            // that mSliceOffset + offset does not overflow.
+            return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
+        }
+
+        @Override
+        public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+            checkChunkValid(offset, size);
+            // checkChunkValid combined with the way instances of this class are constructed ensures
+            // that mSliceOffset + offset does not overflow.
+            dest.put(mArray, (int) (mSliceOffset + offset), size);
+        }
+
+        @Override
+        public DataSource slice(long offset, long size) {
+            checkChunkValid(offset, size);
+            // checkChunkValid combined with the way instances of this class are constructed ensures
+            // that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
+            return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
+        }
+
+        private void checkChunkValid(long offset, long size) {
+            if (offset < 0) {
+                throw new IndexOutOfBoundsException("offset: " + offset);
+            }
+            if (size < 0) {
+                throw new IndexOutOfBoundsException("size: " + size);
+            }
+            if (offset > mSliceSize) {
+                throw new IndexOutOfBoundsException(
+                        "offset (" + offset + ") > source size (" + mSliceSize + ")");
+            }
+            long endOffset = offset + size;
+            if (endOffset < offset) {
+                throw new IndexOutOfBoundsException(
+                        "offset (" + offset + ") + size (" + size + ") overflow");
+            }
+            if (endOffset > mSliceSize) {
+                throw new IndexOutOfBoundsException(
+                        "offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
+                                + ")");
+            }
+        }
+    }
+}

+ 125 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java

@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import com.android.apksig.util.DataSource;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link DataSource} backed by a {@link ByteBuffer}.
+ */
+public class ByteBufferDataSource implements DataSource {
+
+    private final ByteBuffer mBuffer;
+    private final int mSize;
+
+    /**
+     * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
+     * buffer between the buffer's position and limit.
+     */
+    public ByteBufferDataSource(ByteBuffer buffer) {
+        this(buffer, true);
+    }
+
+    /**
+     * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
+     * buffer between the buffer's position and limit.
+     */
+    private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
+        mBuffer = (sliceRequired) ? buffer.slice() : buffer;
+        mSize = buffer.remaining();
+    }
+
+    @Override
+    public long size() {
+        return mSize;
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer(long offset, int size) {
+        checkChunkValid(offset, size);
+
+        // checkChunkValid ensures that it's OK to cast offset to int.
+        int chunkPosition = (int) offset;
+        int chunkLimit = chunkPosition + size;
+        // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
+        // and limit fields, to be more specific). We thus use synchronization around these
+        // state-changing operations to make instances of this class thread-safe.
+        synchronized (mBuffer) {
+            // ByteBuffer.limit(int) and .position(int) check that that the position >= limit
+            // invariant is not broken. Thus, the only way to safely change position and limit
+            // without caring about their current values is to first set position to 0 or set the
+            // limit to capacity.
+            mBuffer.position(0);
+
+            mBuffer.limit(chunkLimit);
+            mBuffer.position(chunkPosition);
+            return mBuffer.slice();
+        }
+    }
+
+    @Override
+    public void copyTo(long offset, int size, ByteBuffer dest) {
+        dest.put(getByteBuffer(offset, size));
+    }
+
+    @Override
+    public void feed(long offset, long size, DataSink sink) throws IOException {
+        if ((size < 0) || (size > mSize)) {
+            throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
+        }
+        sink.consume(getByteBuffer(offset, (int) size));
+    }
+
+    @Override
+    public ByteBufferDataSource slice(long offset, long size) {
+        if ((offset == 0) && (size == mSize)) {
+            return this;
+        }
+        if ((size < 0) || (size > mSize)) {
+            throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
+        }
+        return new ByteBufferDataSource(
+                getByteBuffer(offset, (int) size),
+                false // no need to slice -- it's already a slice
+                );
+    }
+
+    private void checkChunkValid(long offset, long size) {
+        if (offset < 0) {
+            throw new IndexOutOfBoundsException("offset: " + offset);
+        }
+        if (size < 0) {
+            throw new IndexOutOfBoundsException("size: " + size);
+        }
+        if (offset > mSize) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") > source size (" + mSize + ")");
+        }
+        long endOffset = offset + size;
+        if (endOffset < offset) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") + size (" + size + ") overflow");
+        }
+        if (endOffset > mSize) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") + size (" + size + ") > source size (" + mSize  +")");
+        }
+    }
+}

+ 59 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * Data sink which stores all received data into the associated {@link ByteBuffer}.
+ */
+public class ByteBufferSink implements DataSink {
+
+    private final ByteBuffer mBuffer;
+
+    public ByteBufferSink(ByteBuffer buffer) {
+        mBuffer = buffer;
+    }
+
+    public ByteBuffer getBuffer() {
+        return mBuffer;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        try {
+            mBuffer.put(buf, offset, length);
+        } catch (BufferOverflowException e) {
+            throw new IOException(
+                    "Insufficient space in output buffer for " + length + " bytes", e);
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        int length = buf.remaining();
+        try {
+            mBuffer.put(buf);
+        } catch (BufferOverflowException e) {
+            throw new IOException(
+                    "Insufficient space in output buffer for " + length + " bytes", e);
+        }
+    }
+}

+ 33 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import java.nio.ByteBuffer;
+
+public final class ByteBufferUtils {
+    private ByteBufferUtils() {}
+
+    /**
+     * Returns the remaining data of the provided buffer as a new byte array and advances the
+     * position of the buffer to the buffer's limit.
+     */
+    public static byte[] toByteArray(ByteBuffer buf) {
+        byte[] result = new byte[buf.remaining()];
+        buf.get(result);
+        return result;
+    }
+}

+ 41 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Utilities for byte arrays and I/O streams.
+ */
+public final class ByteStreams {
+    private ByteStreams() {}
+
+    /**
+     * Returns the data remaining in the provided input stream as a byte array
+     */
+    public static byte[] toByteArray(InputStream in) throws IOException {
+        ByteArrayOutputStream result = new ByteArrayOutputStream();
+        byte[] buf = new byte[16384];
+        int chunkSize;
+        while ((chunkSize = in.read(buf)) != -1) {
+            result.write(buf, 0, chunkSize);
+        }
+        return result.toByteArray();
+    }
+}

+ 145 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java

@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */
+public class ChainedDataSource implements DataSource {
+
+    private final DataSource[] mSources;
+    private final long mTotalSize;
+
+    public ChainedDataSource(DataSource... sources) {
+        mSources = sources;
+        mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum();
+    }
+
+    @Override
+    public long size() {
+        return mTotalSize;
+    }
+
+    @Override
+    public void feed(long offset, long size, DataSink sink) throws IOException {
+        if (offset + size > mTotalSize) {
+            throw new IndexOutOfBoundsException("Requested more than available");
+        }
+
+        for (DataSource src : mSources) {
+            // Offset is beyond the current source. Skip.
+            if (offset >= src.size()) {
+                offset -= src.size();
+                continue;
+            }
+
+            // If the remaining is enough, finish it.
+            long remaining = src.size() - offset;
+            if (remaining >= size) {
+                src.feed(offset, size, sink);
+                break;
+            }
+
+            // If the remaining is not enough, consume all.
+            src.feed(offset, remaining, sink);
+            size -= remaining;
+            offset = 0;
+        }
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+        if (offset + size > mTotalSize) {
+            throw new IndexOutOfBoundsException("Requested more than available");
+        }
+
+        // Skip to the first DataSource we need.
+        Pair<Integer, Long> firstSource = locateDataSource(offset);
+        int i = firstSource.getFirst();
+        offset = firstSource.getSecond();
+
+        // Return the current source's ByteBuffer if it fits.
+        if (offset + size <= mSources[i].size()) {
+            return mSources[i].getByteBuffer(offset, size);
+        }
+
+        // Otherwise, read into a new buffer.
+        ByteBuffer buffer = ByteBuffer.allocate(size);
+        for (; i < mSources.length && buffer.hasRemaining(); i++) {
+            long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
+            mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
+            offset = 0;  // may not be zero for the first source, but reset after that.
+        }
+        buffer.rewind();
+        return buffer;
+    }
+
+    @Override
+    public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+        feed(offset, size, new ByteBufferSink(dest));
+    }
+
+    @Override
+    public DataSource slice(long offset, long size) {
+        // Find the first slice.
+        Pair<Integer, Long> firstSource = locateDataSource(offset);
+        int beginIndex = firstSource.getFirst();
+        long beginLocalOffset = firstSource.getSecond();
+        DataSource beginSource = mSources[beginIndex];
+
+        if (beginLocalOffset + size <= beginSource.size()) {
+            return beginSource.slice(beginLocalOffset, size);
+        }
+
+        // Add the first slice to chaining, followed by the middle full slices, then the last.
+        ArrayList<DataSource> sources = new ArrayList<>();
+        sources.add(beginSource.slice(
+                beginLocalOffset, beginSource.size() - beginLocalOffset));
+
+        Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
+        int endIndex = lastSource.getFirst();
+        long endLocalOffset = lastSource.getSecond();
+
+        for (int i = beginIndex + 1; i < endIndex; i++) {
+            sources.add(mSources[i]);
+        }
+
+        sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
+        return new ChainedDataSource(sources.toArray(new DataSource[0]));
+    }
+
+    /**
+     * Find the index of DataSource that offset is at.
+     * @return Pair of DataSource index and the local offset in the DataSource.
+     */
+    private Pair<Integer, Long> locateDataSource(long offset) {
+        long localOffset = offset;
+        for (int i = 0; i < mSources.length; i++) {
+            if (localOffset < mSources[i].size()) {
+                return Pair.of(i, localOffset);
+            }
+            localOffset -= mSources[i].size();
+        }
+        throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
+                ", totalSize: " + mTotalSize);
+    }
+}

+ 219 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java

@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2016 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 java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * {@link X509Certificate} which delegates all method invocations to the provided delegate
+ * {@code X509Certificate}.
+ */
+public class DelegatingX509Certificate extends X509Certificate {
+    private static final long serialVersionUID = 1L;
+
+    private final X509Certificate mDelegate;
+
+    public DelegatingX509Certificate(X509Certificate delegate) {
+        this.mDelegate = delegate;
+    }
+
+    @Override
+    public Set<String> getCriticalExtensionOIDs() {
+        return mDelegate.getCriticalExtensionOIDs();
+    }
+
+    @Override
+    public byte[] getExtensionValue(String oid) {
+        return mDelegate.getExtensionValue(oid);
+    }
+
+    @Override
+    public Set<String> getNonCriticalExtensionOIDs() {
+        return mDelegate.getNonCriticalExtensionOIDs();
+    }
+
+    @Override
+    public boolean hasUnsupportedCriticalExtension() {
+        return mDelegate.hasUnsupportedCriticalExtension();
+    }
+
+    @Override
+    public void checkValidity()
+            throws CertificateExpiredException, CertificateNotYetValidException {
+        mDelegate.checkValidity();
+    }
+
+    @Override
+    public void checkValidity(Date date)
+            throws CertificateExpiredException, CertificateNotYetValidException {
+        mDelegate.checkValidity(date);
+    }
+
+    @Override
+    public int getVersion() {
+        return mDelegate.getVersion();
+    }
+
+    @Override
+    public BigInteger getSerialNumber() {
+        return mDelegate.getSerialNumber();
+    }
+
+    @Override
+    public Principal getIssuerDN() {
+        return mDelegate.getIssuerDN();
+    }
+
+    @Override
+    public Principal getSubjectDN() {
+        return mDelegate.getSubjectDN();
+    }
+
+    @Override
+    public Date getNotBefore() {
+        return mDelegate.getNotBefore();
+    }
+
+    @Override
+    public Date getNotAfter() {
+        return mDelegate.getNotAfter();
+    }
+
+    @Override
+    public byte[] getTBSCertificate() throws CertificateEncodingException {
+        return mDelegate.getTBSCertificate();
+    }
+
+    @Override
+    public byte[] getSignature() {
+        return mDelegate.getSignature();
+    }
+
+    @Override
+    public String getSigAlgName() {
+        return mDelegate.getSigAlgName();
+    }
+
+    @Override
+    public String getSigAlgOID() {
+        return mDelegate.getSigAlgOID();
+    }
+
+    @Override
+    public byte[] getSigAlgParams() {
+        return mDelegate.getSigAlgParams();
+    }
+
+    @Override
+    public boolean[] getIssuerUniqueID() {
+        return mDelegate.getIssuerUniqueID();
+    }
+
+    @Override
+    public boolean[] getSubjectUniqueID() {
+        return mDelegate.getSubjectUniqueID();
+    }
+
+    @Override
+    public boolean[] getKeyUsage() {
+        return mDelegate.getKeyUsage();
+    }
+
+    @Override
+    public int getBasicConstraints() {
+        return mDelegate.getBasicConstraints();
+    }
+
+    @Override
+    public byte[] getEncoded() throws CertificateEncodingException {
+        return mDelegate.getEncoded();
+    }
+
+    @Override
+    public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
+            InvalidKeyException, NoSuchProviderException, SignatureException {
+        mDelegate.verify(key);
+    }
+
+    @Override
+    public void verify(PublicKey key, String sigProvider)
+            throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
+            NoSuchProviderException, SignatureException {
+        mDelegate.verify(key, sigProvider);
+    }
+
+    @Override
+    public String toString() {
+        return mDelegate.toString();
+    }
+
+    @Override
+    public PublicKey getPublicKey() {
+        return mDelegate.getPublicKey();
+    }
+
+    @Override
+    public X500Principal getIssuerX500Principal() {
+        return mDelegate.getIssuerX500Principal();
+    }
+
+    @Override
+    public X500Principal getSubjectX500Principal() {
+        return mDelegate.getSubjectX500Principal();
+    }
+
+    @Override
+    public List<String> getExtendedKeyUsage() throws CertificateParsingException {
+        return mDelegate.getExtendedKeyUsage();
+    }
+
+    @Override
+    public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
+        return mDelegate.getSubjectAlternativeNames();
+    }
+
+    @Override
+    public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
+        return mDelegate.getIssuerAlternativeNames();
+    }
+
+    @Override
+    @SuppressWarnings("AndroidJdkLibsChecker")
+    public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
+            NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        mDelegate.verify(key, sigProvider);
+    }
+}

+ 191 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java

@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import com.android.apksig.util.DataSource;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
+ */
+public class FileChannelDataSource implements DataSource {
+
+    private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
+
+    private final FileChannel mChannel;
+    private final long mOffset;
+    private final long mSize;
+
+    /**
+     * Constructs a new {@code FileChannelDataSource} based on the data contained in the
+     * whole file. Changes to the contents of the file, including the size of the file,
+     * will be visible in this data source.
+     */
+    public FileChannelDataSource(FileChannel channel) {
+        mChannel = channel;
+        mOffset = 0;
+        mSize = -1;
+    }
+
+    /**
+     * Constructs a new {@code FileChannelDataSource} based on the data contained in the
+     * specified region of the provided file. Changes to the contents of the file will be visible in
+     * this data source.
+     *
+     * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
+     */
+    public FileChannelDataSource(FileChannel channel, long offset, long size) {
+        if (offset < 0) {
+            throw new IndexOutOfBoundsException("offset: " + size);
+        }
+        if (size < 0) {
+            throw new IndexOutOfBoundsException("size: " + size);
+        }
+        mChannel = channel;
+        mOffset = offset;
+        mSize = size;
+    }
+
+    @Override
+    public long size() {
+        if (mSize == -1) {
+            try {
+                return mChannel.size();
+            } catch (IOException e) {
+                return 0;
+            }
+        } else {
+            return mSize;
+        }
+    }
+
+    @Override
+    public FileChannelDataSource slice(long offset, long size) {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if ((offset == 0) && (size == sourceSize)) {
+            return this;
+        }
+
+        return new FileChannelDataSource(mChannel, mOffset + offset, size);
+    }
+
+    @Override
+    public void feed(long offset, long size, DataSink sink) throws IOException {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if (size == 0) {
+            return;
+        }
+
+        long chunkOffsetInFile = mOffset + offset;
+        long remaining = size;
+        ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
+
+        while (remaining > 0) {
+            int chunkSize = (int) Math.min(remaining, buf.capacity());
+            int chunkRemaining = chunkSize;
+            buf.limit(chunkSize);
+            synchronized (mChannel) {
+                mChannel.position(chunkOffsetInFile);
+                while (chunkRemaining > 0) {
+                    int read = mChannel.read(buf);
+                    if (read < 0) {
+                        throw new IOException("Unexpected EOF encountered");
+                    }
+                    chunkRemaining -= read;
+                }
+            }
+            buf.flip();
+            sink.consume(buf);
+            buf.clear();
+            chunkOffsetInFile += chunkSize;
+            remaining -= chunkSize;
+        }
+    }
+
+    @Override
+    public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if (size == 0) {
+            return;
+        }
+        if (size > dest.remaining()) {
+            throw new BufferOverflowException();
+        }
+
+        long offsetInFile = mOffset + offset;
+        int remaining = size;
+        int prevLimit = dest.limit();
+        try {
+            // FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
+            // the buffer's limit to avoid reading more than size bytes.
+            dest.limit(dest.position() + size);
+            while (remaining > 0) {
+                int chunkSize;
+                synchronized (mChannel) {
+                    mChannel.position(offsetInFile);
+                    chunkSize = mChannel.read(dest);
+                }
+                offsetInFile += chunkSize;
+                remaining -= chunkSize;
+            }
+        } finally {
+            dest.limit(prevLimit);
+        }
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+        if (size < 0) {
+            throw new IndexOutOfBoundsException("size: " + size);
+        }
+        ByteBuffer result = ByteBuffer.allocate(size);
+        copyTo(offset, size, result);
+        result.flip();
+        return result;
+    }
+
+    private static void checkChunkValid(long offset, long size, long sourceSize) {
+        if (offset < 0) {
+            throw new IndexOutOfBoundsException("offset: " + offset);
+        }
+        if (size < 0) {
+            throw new IndexOutOfBoundsException("size: " + size);
+        }
+        if (offset > sourceSize) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") > source size (" + sourceSize + ")");
+        }
+        long endOffset = offset + size;
+        if (endOffset < offset) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") + size (" + size + ") overflow");
+        }
+        if (endOffset > sourceSize) {
+            throw new IndexOutOfBoundsException(
+                    "offset (" + offset + ") + size (" + size
+                            + ") > source size (" + sourceSize  +")");
+        }
+    }
+}

+ 68 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+/**
+ * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
+ * time.
+ */
+public class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
+    private static final long serialVersionUID = 1L;
+
+    private final byte[] mEncodedForm;
+    private int mHash = -1;
+
+    public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
+        super(wrapped);
+        this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
+    }
+
+    @Override
+    public byte[] getEncoded() throws CertificateEncodingException {
+        return (mEncodedForm != null) ? mEncodedForm.clone() : null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof X509Certificate)) return false;
+
+        try {
+            byte[] a = this.getEncoded();
+            byte[] b = ((X509Certificate) o).getEncoded();
+            return Arrays.equals(a, b);
+        } catch (CertificateEncodingException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHash == -1) {
+            try {
+                mHash = Arrays.hashCode(this.getEncoded());
+            } catch (CertificateEncodingException e) {
+                mHash = 0;
+            }
+        }
+        return mHash;
+    }
+}

+ 89 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Inclusive interval of integers.
+ */
+public class InclusiveIntRange {
+    private final int min;
+    private final int max;
+
+    private InclusiveIntRange(int min, int max) {
+        this.min = min;
+        this.max = max;
+    }
+
+    public int getMin() {
+        return min;
+    }
+
+    public int getMax() {
+        return max;
+    }
+
+    public static InclusiveIntRange fromTo(int min, int max) {
+        return new InclusiveIntRange(min, max);
+    }
+
+    public static InclusiveIntRange from(int min) {
+        return new InclusiveIntRange(min, Integer.MAX_VALUE);
+    }
+
+    public List<InclusiveIntRange> getValuesNotIn(
+            List<InclusiveIntRange> sortedNonOverlappingRanges) {
+        if (sortedNonOverlappingRanges.isEmpty()) {
+            return Collections.singletonList(this);
+        }
+
+        int testValue = min;
+        List<InclusiveIntRange> result = null;
+        for (InclusiveIntRange range : sortedNonOverlappingRanges) {
+            int rangeMax = range.max;
+            if (testValue > rangeMax) {
+                continue;
+            }
+            int rangeMin = range.min;
+            if (testValue < range.min) {
+                if (result == null) {
+                    result = new ArrayList<>();
+                }
+                result.add(fromTo(testValue, rangeMin - 1));
+            }
+            if (rangeMax >= max) {
+                return (result != null) ? result : Collections.emptyList();
+            }
+            testValue = rangeMax + 1;
+        }
+        if (testValue <= max) {
+            if (result == null) {
+                result = new ArrayList<>(1);
+            }
+            result.add(fromTo(testValue, max));
+        }
+        return (result != null) ? result : Collections.emptyList();
+    }
+
+    @Override
+    public String toString() {
+        return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)");
+    }
+}

+ 51 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+/**
+ * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
+ * {@code MessageDigest} instance receives the same data.
+ */
+public class MessageDigestSink implements DataSink {
+
+    private final MessageDigest[] mMessageDigests;
+
+    public MessageDigestSink(MessageDigest[] digests) {
+        mMessageDigests = digests;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) {
+        for (MessageDigest md : mMessageDigests) {
+            md.update(buf, offset, length);
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) {
+        int originalPosition = buf.position();
+        for (MessageDigest md : mMessageDigests) {
+            // Reset the position back to the original because the previous iteration's
+            // MessageDigest.update set the buffer's position to the buffer's limit.
+            buf.position(originalPosition);
+            md.update(buf);
+        }
+    }
+}

+ 77 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link DataSink} which outputs received data into the associated {@link OutputStream}.
+ */
+public class OutputStreamDataSink implements DataSink {
+
+    private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+    private final OutputStream mOut;
+
+    /**
+     * Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided
+     * {@link OutputStream}.
+     */
+    public OutputStreamDataSink(OutputStream out) {
+        if (out == null) {
+            throw new NullPointerException("out == null");
+        }
+        mOut = out;
+    }
+
+    /**
+     * Returns {@link OutputStream} into which this data sink outputs received data.
+     */
+    public OutputStream getOutputStream() {
+        return mOut;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        mOut.write(buf, offset, length);
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        if (!buf.hasRemaining()) {
+            return;
+        }
+
+        if (buf.hasArray()) {
+            mOut.write(
+                    buf.array(),
+                    buf.arrayOffset() + buf.position(),
+                    buf.remaining());
+            buf.position(buf.limit());
+        } else {
+            byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
+            while (buf.hasRemaining()) {
+                int chunkSize = Math.min(buf.remaining(), tmp.length);
+                buf.get(tmp, 0, chunkSize);
+                mOut.write(tmp, 0, chunkSize);
+            }
+        }
+    }
+}

+ 81 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 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;
+
+/**
+ * Pair of two elements.
+ */
+public final class Pair<A, B> {
+    private final A mFirst;
+    private final B mSecond;
+
+    private Pair(A first, B second) {
+        mFirst = first;
+        mSecond = second;
+    }
+
+    public static <A, B> Pair<A, B> of(A first, B second) {
+        return new Pair<A, B>(first, second);
+    }
+
+    public A getFirst() {
+        return mFirst;
+    }
+
+    public B getSecond() {
+        return mSecond;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
+        result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        @SuppressWarnings("rawtypes")
+        Pair other = (Pair) obj;
+        if (mFirst == null) {
+            if (other.mFirst != null) {
+                return false;
+            }
+        } else if (!mFirst.equals(other.mFirst)) {
+            return false;
+        }
+        if (mSecond == null) {
+            if (other.mSecond != null) {
+                return false;
+            }
+        } else if (!mSecond.equals(other.mSecond)) {
+            return false;
+        }
+        return true;
+    }
+}

+ 104 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 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.util.DataSink;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * {@link DataSink} which outputs received data into the associated file, sequentially.
+ */
+public class RandomAccessFileDataSink implements DataSink {
+
+    private final RandomAccessFile mFile;
+    private final FileChannel mFileChannel;
+    private long mPosition;
+
+    /**
+     * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
+     * beginning of the provided file.
+     */
+    public RandomAccessFileDataSink(RandomAccessFile file) {
+        this(file, 0);
+    }
+
+    /**
+     * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
+     * specified position of the provided file.
+     */
+    public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) {
+        if (file == null) {
+            throw new NullPointerException("file == null");
+        }
+        if (startPosition < 0) {
+            throw new IllegalArgumentException("startPosition: " + startPosition);
+        }
+        mFile = file;
+        mFileChannel = file.getChannel();
+        mPosition = startPosition;
+    }
+
+    /**
+     * Returns the underlying {@link RandomAccessFile}.
+     */
+    public RandomAccessFile getFile() {
+        return mFile;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        if (offset < 0) {
+            // Must perform this check here because RandomAccessFile.write doesn't throw when offset
+            // is negative but length is 0
+            throw new IndexOutOfBoundsException("offset: " + offset);
+        }
+        if (offset > buf.length) {
+            // Must perform this check here because RandomAccessFile.write doesn't throw when offset
+            // is too large but length is 0
+            throw new IndexOutOfBoundsException(
+                    "offset: " + offset + ", buf.length: " + buf.length);
+        }
+        if (length == 0) {
+            return;
+        }
+
+        synchronized (mFile) {
+            mFile.seek(mPosition);
+            mFile.write(buf, offset, length);
+            mPosition += length;
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        int length = buf.remaining();
+        if (length == 0) {
+            return;
+        }
+
+        synchronized (mFile) {
+            mFile.seek(mPosition);
+            while (buf.hasRemaining()) {
+                mFileChannel.write(buf);
+            }
+            mPosition += length;
+        }
+    }
+}

+ 51 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import com.android.apksig.util.DataSink;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link DataSink} which copies provided input into each of the sinks provided to it.
+ */
+public class TeeDataSink implements DataSink {
+
+    private final DataSink[] mSinks;
+
+    public TeeDataSink(DataSink[] sinks) {
+        mSinks = sinks;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        for (DataSink sink : mSinks) {
+            sink.consume(buf, offset, length);
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        int originalPosition = buf.position();
+        for (int i = 0; i < mSinks.length; i++) {
+            if (i > 0) {
+                buf.position(originalPosition);
+            }
+            mSinks[i].consume(buf);
+        }
+    }
+}

+ 325 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java

@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import java.util.ArrayList;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * VerityTreeBuilder is used to generate the root hash of verity tree built from the input file.
+ * The root hash can be used on device for on-access verification.  The tree itself is reproducible
+ * on device, and is not shipped with the APK.
+ */
+public class VerityTreeBuilder implements AutoCloseable {
+
+    /**
+     * Maximum size (in bytes) of each node of the tree.
+     */
+    private final static int CHUNK_SIZE = 4096;
+    /**
+     * Maximum parallelism while calculating digests.
+     */
+    private final static int DIGEST_PARALLELISM = Math.min(32,
+            Runtime.getRuntime().availableProcessors());
+    /**
+     * Queue size.
+     */
+    private final static int MAX_OUTSTANDING_CHUNKS = 4;
+    /**
+     * Typical prefetch size.
+     */
+    private final static int MAX_PREFETCH_CHUNKS = 1024;
+    /**
+     * Minimum chunks to be processed by a single worker task.
+     */
+    private final static int MIN_CHUNKS_PER_WORKER = 8;
+
+    /**
+     * Digest algorithm (JCA Digest algorithm name) used in the tree.
+     */
+    private final static String JCA_ALGORITHM = "SHA-256";
+
+    /**
+     * Optional salt to apply before each digestion.
+     */
+    private final byte[] mSalt;
+
+    private final MessageDigest mMd;
+
+    private final ExecutorService mExecutor =
+            new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM,
+                    0L, MILLISECONDS,
+                    new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS),
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+
+    public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException {
+        mSalt = salt;
+        mMd = getNewMessageDigest();
+    }
+
+    @Override
+    public void close() {
+        mExecutor.shutdownNow();
+    }
+
+    /**
+     * Returns the root hash of the APK verity tree built from ZIP blocks.
+     *
+     * Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which
+     * must be page aligned) and the "Central Directory offset" field in End of Central Directory
+     * are skipped.
+     */
+    public byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock,
+            DataSource centralDir, DataSource eocd) throws IOException {
+        if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) {
+            throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE
+                    + ": " + beforeApkSigningBlock.size());
+        }
+
+        // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
+        // offset field is treated as pointing to the offset at which the APK Signing Block will
+        // start.
+        long centralDirOffsetForDigesting = beforeApkSigningBlock.size();
+        ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        eocd.copyTo(0, (int) eocd.size(), eocdBuf);
+        eocdBuf.flip();
+        ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
+
+        return generateVerityTreeRootHash(new ChainedDataSource(beforeApkSigningBlock, centralDir,
+                    DataSources.asDataSource(eocdBuf)));
+    }
+
+    /**
+     * Returns the root hash of the verity tree built from the data source.
+     */
+    public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+        ByteBuffer verityBuffer = generateVerityTree(fileSource);
+        return getRootHashFromTree(verityBuffer);
+    }
+
+    /**
+     * Returns the byte buffer that contains the whole verity tree.
+     *
+     * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
+     * input file.  If the total size is larger than 4 KB, take this level as input and repeat the
+     * same procedure, until the level is within 4 KB.  If salt is given, it will apply to each
+     * digestion before the actual data.
+     *
+     * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
+     *
+     * 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) throws IOException {
+        int digestSize = mMd.getDigestLength();
+
+        // Calculate the summed area table of level size. In other word, this is the offset
+        // table of each level, plus the next non-existing level.
+        int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize);
+
+        ByteBuffer verityBuffer = ByteBuffer.allocate(levelOffset[levelOffset.length - 1]);
+
+        // Generate the hash tree bottom-up.
+        for (int i = levelOffset.length - 2; i >= 0; i--) {
+            DataSink middleBufferSink = new ByteBufferSink(
+                    slice(verityBuffer, levelOffset[i], levelOffset[i + 1]));
+            DataSource src;
+            if (i == levelOffset.length - 2) {
+                src = fileSource;
+                digestDataByChunks(src, middleBufferSink);
+            } else {
+                src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
+                        levelOffset[i + 1], levelOffset[i + 2]));
+                digestDataByChunks(src, middleBufferSink);
+            }
+
+            // If the output is not full chunk, pad with 0s.
+            long totalOutput = divideRoundup(src.size(), CHUNK_SIZE) * digestSize;
+            int incomplete = (int) (totalOutput % CHUNK_SIZE);
+            if (incomplete > 0) {
+                byte[] padding = new byte[CHUNK_SIZE - incomplete];
+                middleBufferSink.consume(padding, 0, padding.length);
+            }
+        }
+        return verityBuffer;
+    }
+
+    /**
+     * Returns the digested root hash from the top level (only page) of a verity tree.
+     */
+    public byte[] getRootHashFromTree(ByteBuffer verityBuffer) throws IOException {
+        ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE);
+        return saltedDigest(firstPage);
+    }
+
+    /**
+     * Returns an array of summed area table of level size in the verity tree.  In other words, the
+     * returned array is offset of each level in the verity tree file format, plus an additional
+     * offset of the next non-existing level (i.e. end of the last level + 1).  Thus the array size
+     * is level + 1.
+     */
+    private static int[] calculateLevelOffset(long dataSize, int digestSize) {
+        // Compute total size of each level, bottom to top.
+        ArrayList<Long> levelSize = new ArrayList<>();
+        while (true) {
+            long chunkCount = divideRoundup(dataSize, CHUNK_SIZE);
+            long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize, CHUNK_SIZE);
+            levelSize.add(size);
+            if (chunkCount * digestSize <= CHUNK_SIZE) {
+                break;
+            }
+            dataSize = chunkCount * digestSize;
+        }
+
+        // Reverse and convert to summed area table.
+        int[] levelOffset = new int[levelSize.size() + 1];
+        levelOffset[0] = 0;
+        for (int i = 0; i < levelSize.size(); i++) {
+            // We don't support verity tree if it is larger then Integer.MAX_VALUE.
+            levelOffset[i + 1] = levelOffset[i] + Math.toIntExact(
+                    levelSize.get(levelSize.size() - i - 1));
+        }
+        return levelOffset;
+    }
+
+    /**
+     * Digest data source by chunks then feeds them to the sink one by one.  If the last unit is
+     * 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) throws IOException {
+        final long size = dataSource.size();
+        final int chunks = (int) divideRoundup(size, CHUNK_SIZE);
+
+        /** Single IO operation size, in chunks. */
+        final int ioSizeChunks = MAX_PREFETCH_CHUNKS;
+
+        final byte[][] hashes = new byte[chunks][];
+
+        Phaser tasks = new Phaser(1);
+
+        // Reading the input file as fast as we can.
+        final long maxReadSize = ioSizeChunks * CHUNK_SIZE;
+
+        long readOffset = 0;
+        int startChunkIndex = 0;
+        while (readOffset < size) {
+            final long readLimit = Math.min(readOffset + maxReadSize, size);
+            final int readSize = (int) (readLimit - readOffset);
+            final int bufferSizeChunks = (int) divideRoundup(readSize, CHUNK_SIZE);
+
+            // Overllocating to zero-pad last chunk.
+            // With 4MiB block size, 32 threads and 4 queue size we might allocate up to 144MiB.
+            final ByteBuffer buffer = ByteBuffer.allocate(bufferSizeChunks * CHUNK_SIZE);
+            dataSource.copyTo(readOffset, readSize, buffer);
+            buffer.rewind();
+
+            final int readChunkIndex = startChunkIndex;
+            Runnable task = () -> {
+                final MessageDigest md = cloneMessageDigest();
+                for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex;
+                        offset < finish; offset += CHUNK_SIZE, ++chunkIndex) {
+                    ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE);
+                    hashes[chunkIndex] = saltedDigest(md, chunk);
+                }
+                tasks.arriveAndDeregister();
+            };
+            tasks.register();
+            mExecutor.execute(task);
+
+            startChunkIndex += bufferSizeChunks;
+            readOffset += readSize;
+        }
+
+        // Waiting for the tasks to complete.
+        tasks.arriveAndAwaitAdvance();
+
+        // Streaming hashes back.
+        for (byte[] hash : hashes) {
+            dataSink.consume(hash, 0, hash.length);
+        }
+    }
+
+    /** Returns the digest of data with salt prepended. */
+    private byte[] saltedDigest(ByteBuffer data) {
+        return saltedDigest(mMd, data);
+    }
+
+    private byte[] saltedDigest(MessageDigest md, ByteBuffer data) {
+        md.reset();
+        if (mSalt != null) {
+            md.update(mSalt);
+        }
+        md.update(data);
+        return md.digest();
+    }
+
+    /** Divides a number and round up to the closest integer. */
+    private static long divideRoundup(long dividend, long divisor) {
+        return (dividend + divisor - 1) / divisor;
+    }
+
+    /** Returns a slice of the buffer with shared the content. */
+    private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) {
+        ByteBuffer b = buffer.duplicate();
+        b.position(0);  // to ensure position <= limit invariant.
+        b.limit(end);
+        b.position(begin);
+        return b.slice();
+    }
+
+    /**
+     * Obtains a new instance of the message digest algorithm.
+     */
+    private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance(JCA_ALGORITHM);
+    }
+
+    /**
+     * Clones the existing message digest, or creates a new instance if clone is unavailable.
+     */
+    private MessageDigest cloneMessageDigest() {
+        try {
+            return (MessageDigest) mMd.clone();
+        } catch (CloneNotSupportedException ignored) {
+            try {
+                return getNewMessageDigest();
+            } catch (NoSuchAlgorithmException e) {
+                throw new IllegalStateException(
+                        "Failed to obtain an instance of a previously available message digest", e);
+            }
+        }
+    }
+}

+ 282 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java

@@ -0,0 +1,282 @@
+/*
+ * 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 volatile 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;
+        }
+
+        buildCertFactoryHelper();
+    }
+
+    private static synchronized void buildCertFactoryHelper() {
+        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 {
+        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 {
+        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);
+    }
+}

+ 35 - 0
platform/android/java/editor/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;
+}

+ 105 - 0
platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java

@@ -0,0 +1,105 @@
+/*
+ * 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;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * 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;
+
+    public static X509Certificate findCertificate(
+            Collection<X509Certificate> certs, SignerIdentifier id) {
+        for (X509Certificate cert : certs) {
+            if (isMatchingCerticicate(cert, id)) {
+                return cert;
+            }
+        }
+        return null;
+    }
+
+    private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) {
+        if (id.issuerAndSerialNumber == null) {
+            // Android doesn't support any other means of identifying the signing certificate
+            return false;
+        }
+        IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber;
+        byte[] encodedIssuer =
+                ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded());
+        X500Principal idIssuer = new X500Principal(encodedIssuer);
+        BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber;
+        return idSerialNumber.equals(cert.getSerialNumber())
+                && idIssuer.equals(cert.getIssuerX500Principal());
+    }
+
+    public static List<X509Certificate> parseCertificates(
+            List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
+        if (encodedCertificates.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        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 = X509CertificateUtils.generateCertificate(encodedForm);
+            } catch (CertificateException e) {
+                throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
+            }
+            // 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 and/or some implementations of
+            // X509Certificate.getEncoded() re-encode certificates.
+            certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
+            result.add(certificate);
+        }
+        return result;
+    }
+}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor