| /* |
| * 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 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.jar.ManifestParser; |
| import com.android.apksig.internal.util.AndroidSdkVersion; |
| import com.android.apksig.internal.util.InclusiveIntRange; |
| import com.android.apksig.internal.util.MessageDigestSink; |
| import com.android.apksig.internal.zip.CentralDirectoryRecord; |
| import com.android.apksig.internal.zip.LocalFileRecord; |
| 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.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SignatureException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.X509Certificate; |
| 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; |
| import sun.security.pkcs.PKCS7; |
| import sun.security.pkcs.SignerInfo; |
| |
| /** |
| * 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 static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME; |
| |
| 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()}. |
| * |
| * @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; |
| } |
| |
| /** |
| * 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) && (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); |
| } |
| Map<String, ManifestParser.Section> entryNameToManifestSection = null; |
| ManifestParser manifest = new ManifestParser(manifestBytes); |
| ManifestParser.Section manifestMainSection = manifest.readSection(); |
| List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections(); |
| 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; |
| } |
| } |
| if (result.containsErrors()) { |
| return; |
| } |
| // 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; |
| } |
| |
| // 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; |
| } |
| |
| @SuppressWarnings("restriction") |
| public void verifySigBlockAgainstSigFile( |
| DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion) |
| throws IOException, ApkFormatException, NoSuchAlgorithmException { |
| byte[] sigBlockBytes; |
| try { |
| sigBlockBytes = |
| LocalFileRecord.getUncompressedData( |
| apk, mSignatureBlockEntry, cdStartOffset); |
| } catch (ZipFormatException e) { |
| throw new ApkFormatException( |
| "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e); |
| } |
| try { |
| mSigFileBytes = |
| LocalFileRecord.getUncompressedData( |
| apk, mSignatureFileEntry, cdStartOffset); |
| } catch (ZipFormatException e) { |
| throw new ApkFormatException( |
| "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e); |
| } |
| PKCS7 sigBlock; |
| try { |
| sigBlock = new PKCS7(sigBlockBytes); |
| } catch (IOException e) { |
| if (e.getCause() instanceof CertificateException) { |
| mResult.addError( |
| Issue.JAR_SIG_MALFORMED_CERTIFICATE, mSignatureBlockEntry.getName(), e); |
| } else { |
| mResult.addError( |
| Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); |
| } |
| return; |
| } |
| SignerInfo[] unverifiedSignerInfos = sigBlock.getSignerInfos(); |
| if ((unverifiedSignerInfos == null) || (unverifiedSignerInfos.length == 0)) { |
| mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); |
| return; |
| } |
| |
| SignerInfo verifiedSignerInfo = null; |
| if ((unverifiedSignerInfos != null) && (unverifiedSignerInfos.length > 0)) { |
| for (int i = 0; i < unverifiedSignerInfos.length; i++) { |
| SignerInfo unverifiedSignerInfo = unverifiedSignerInfos[i]; |
| String digestAlgorithmOid = |
| unverifiedSignerInfo.getDigestAlgorithmId().getOID().toString(); |
| String signatureAlgorithmOid = |
| unverifiedSignerInfo |
| .getDigestEncryptionAlgorithmId().getOID().toString(); |
| InclusiveIntRange desiredApiLevels = |
| InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion); |
| List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported = |
| getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid); |
| List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported = |
| desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported); |
| if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) { |
| mResult.addError( |
| Issue.JAR_SIG_UNSUPPORTED_SIG_ALG, |
| mSignatureBlockEntry.getName(), |
| digestAlgorithmOid, |
| signatureAlgorithmOid, |
| String.valueOf(apiLevelsWhereDigestAlgorithmNotSupported)); |
| return; |
| } |
| try { |
| verifiedSignerInfo = sigBlock.verify(unverifiedSignerInfo, mSigFileBytes); |
| } catch (SignatureException e) { |
| mResult.addError( |
| Issue.JAR_SIG_VERIFY_EXCEPTION, |
| mSignatureBlockEntry.getName(), |
| mSignatureFileEntry.getName(), |
| e); |
| return; |
| } |
| if (verifiedSignerInfo != null) { |
| // Verified |
| break; |
| } |
| |
| // Did not verify |
| if (minSdkVersion < AndroidSdkVersion.N) { |
| // Prior to N, Android attempted to verify only the first SignerInfo. |
| mResult.addError( |
| Issue.JAR_SIG_DID_NOT_VERIFY, |
| mSignatureBlockEntry.getName(), |
| mSignatureFileEntry.getName()); |
| return; |
| } |
| } |
| } |
| if (verifiedSignerInfo == null) { |
| mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); |
| return; |
| } |
| |
| // TODO: PKCS7 class doesn't guarantee that returned certificates' getEncoded returns |
| // the original encoded form of certificates rather than the DER re-encoded form. We |
| // need to replace the PKCS7 parser/verifier. |
| List<X509Certificate> certChain; |
| try { |
| certChain = verifiedSignerInfo.getCertificateChain(sigBlock); |
| } catch (IOException e) { |
| throw new RuntimeException( |
| "Failed to obtain cert chain from " + mSignatureBlockEntry.getName(), e); |
| } |
| if ((certChain == null) || (certChain.isEmpty())) { |
| throw new RuntimeException("Verified SignerInfo does not have a certificate chain"); |
| } |
| mResult.certChain.clear(); |
| mResult.certChain.addAll(certChain); |
| } |
| |
| private static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5"; |
| static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26"; |
| private static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4"; |
| static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1"; |
| private static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2"; |
| private static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3"; |
| |
| static final String OID_SIG_RSA = "1.2.840.113549.1.1.1"; |
| private static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4"; |
| private static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5"; |
| private static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14"; |
| private static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11"; |
| private static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12"; |
| private static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13"; |
| |
| static final String OID_SIG_DSA = "1.2.840.10040.4.1"; |
| private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3"; |
| private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1"; |
| static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2"; |
| |
| static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1"; |
| private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1"; |
| private static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1"; |
| private static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2"; |
| private static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3"; |
| private static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4"; |
| |
| private static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS = |
| new HashMap<>(); |
| { |
| 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)); |
| } |
| |
| private static void addSupportedSigAlg( |
| String digestAlgorithmOid, |
| String signatureAlgorithmOid, |
| InclusiveIntRange... supportedApiLevels) { |
| SUPPORTED_SIG_ALG_OIDS.put( |
| digestAlgorithmOid + "with" + signatureAlgorithmOid, |
| Arrays.asList(supportedApiLevels)); |
| } |
| |
| private 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 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, |
| V1SchemeSigner.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( |
| V1SchemeSigner.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); |
| } |
| } |
| } |
| } |
| |
| private 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 { |
| // Read the ZIP Central Directory |
| 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); |
| |
| // Parse the ZIP Central Directory |
| int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); |
| List<CentralDirectoryRecord> cdRecords = new ArrayList<>(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 (entryName.endsWith("/")) { |
| // Ignore directory entries |
| continue; |
| } |
| cdRecords.add(cdRecord); |
| } |
| // There may be more data in Central Directory, but we don't warn or throw because Android |
| // ignores unused CD data. |
| |
| return cdRecords; |
| } |
| |
| /** |
| * 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); |
| Set<String> manifestEntryNamesMissingFromApk = |
| new HashSet<>(entryNameToManifestSection.keySet()); |
| List<Signer> firstSignedEntrySigners = null; |
| String firstSignedEntryName = null; |
| for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { |
| String entryName = cdRecord.getName(); |
| manifestEntryNamesMissingFromApk.remove(entryName); |
| 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, |
| new MessageDigestSink(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, |
| V1SchemeSigner.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); |
| } |
| |
| private static class NamedDigest { |
| private final String jcaDigestAlgorithm; |
| private 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; |
| } |
| } |
| } |
| } |