| /* |
| * 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.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.minSdkVersion, 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 { |
| if (currentMin != lastMax + 1) { |
| 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()); |
| } |
| } |
| |
| /** 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; |
| } |
| } |
| } |