All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.util.apk.SourceStampVerifier Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * 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 android.util.apk;

import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct;

import android.util.Pair;
import android.util.Slog;
import android.util.jar.StrictJarFile;

import libcore.io.Streams;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
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.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
 * Source Stamp verifier.
 *
 * 

SourceStamp improves traceability of apps with respect to unauthorized distribution. * *

The stamp is part of the APK that is protected by the signing block. * *

The APK contents hash is signed using the stamp key, and is saved as part of the signing * block. * * @hide for internal use only. */ public abstract class SourceStampVerifier { private static final String TAG = "SourceStampVerifier"; private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; private static final int SOURCE_STAMP_BLOCK_ID = 0x6dff800d; private static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7; private static final int VERSION_JAR_SIGNATURE_SCHEME = 1; private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; private static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; /** Hidden constructor to prevent instantiation. */ private SourceStampVerifier() { } /** Verifies SourceStamp present in a list of (split) APKs for the same app. */ public static SourceStampVerificationResult verify(List apkFiles) { Certificate stampCertificate = null; List stampCertificateLineage = Collections.emptyList(); for (String apkFile : apkFiles) { SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile); if (!sourceStampVerificationResult.isPresent() || !sourceStampVerificationResult.isVerified()) { return sourceStampVerificationResult; } if (stampCertificate != null && (!stampCertificate.equals(sourceStampVerificationResult.getCertificate()) || !stampCertificateLineage.equals( sourceStampVerificationResult.getCertificateLineage()))) { return SourceStampVerificationResult.notVerified(); } stampCertificate = sourceStampVerificationResult.getCertificate(); stampCertificateLineage = sourceStampVerificationResult.getCertificateLineage(); } return SourceStampVerificationResult.verified(stampCertificate, stampCertificateLineage); } /** Verifies SourceStamp present in the provided APK. */ public static SourceStampVerificationResult verify(String apkFile) { StrictJarFile apkJar = null; try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { apkJar = new StrictJarFile( apkFile, /* verify= */ false, /* signatureSchemeRollbackProtectionsEnforced= */ false); byte[] sourceStampCertificateDigest = getSourceStampCertificateDigest(apkJar); if (sourceStampCertificateDigest == null) { // SourceStamp certificate hash file not found, which means that there is not // SourceStamp present. return SourceStampVerificationResult.notPresent(); } byte[] manifestBytes = getManifestBytes(apkJar); return verify(apk, sourceStampCertificateDigest, manifestBytes); } catch (IOException e) { // Any exception in reading the APK returns a non-present SourceStamp outcome // without affecting the outcome of any of the other signature schemes. return SourceStampVerificationResult.notPresent(); } finally { closeApkJar(apkJar); } } private static SourceStampVerificationResult verify( RandomAccessFile apk, byte[] sourceStampCertificateDigest, byte[] manifestBytes) { try { SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID); Map> signatureSchemeApkContentDigests = getSignatureSchemeApkContentDigests(apk, manifestBytes); return verify( signatureInfo, getSignatureSchemeDigests(signatureSchemeApkContentDigests), sourceStampCertificateDigest); } catch (IOException | SignatureNotFoundException | RuntimeException e) { return SourceStampVerificationResult.notVerified(); } } private static SourceStampVerificationResult verify( SignatureInfo signatureInfo, Map signatureSchemeDigests, byte[] sourceStampCertificateDigest) throws SecurityException, IOException { ByteBuffer sourceStampBlock = signatureInfo.signatureBlock; ByteBuffer sourceStampBlockData = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); X509Certificate sourceStampCertificate = verifySourceStampCertificate(sourceStampBlockData, sourceStampCertificateDigest); // Parse signed signature schemes block. ByteBuffer signedSignatureSchemes = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); Map signedSignatureSchemeData = new HashMap<>(); while (signedSignatureSchemes.hasRemaining()) { ByteBuffer signedSignatureScheme = ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes); int signatureSchemeId = signedSignatureScheme.getInt(); signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme); } for (Map.Entry signatureSchemeDigest : signatureSchemeDigests.entrySet()) { if (!signedSignatureSchemeData.containsKey(signatureSchemeDigest.getKey())) { throw new SecurityException( String.format( "No signatures found for signature scheme %d", signatureSchemeDigest.getKey())); } ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice( signedSignatureSchemeData.get(signatureSchemeDigest.getKey())); verifySourceStampSignature( signatureSchemeDigest.getValue(), sourceStampCertificate, signatures); } List sourceStampCertificateLineage = Collections.emptyList(); 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, sourceStampCertificate, stampAttributeDataSignatures); ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = verifySourceStampAttributes(stampAttributeData, sourceStampCertificate); if (verifiedProofOfRotation != null) { sourceStampCertificateLineage = verifiedProofOfRotation.certs; } } return SourceStampVerificationResult.verified(sourceStampCertificate, sourceStampCertificateLineage); } /** * Verify the SourceStamp certificate found in the signing block is the same as the SourceStamp * certificate found in the APK. It returns the verified certificate. * * @param sourceStampBlockData the source stamp block in the APK signing block which * contains * the certificate used to sign the stamp digests. * @param sourceStampCertificateDigest the source stamp certificate digest found in the APK. */ private static X509Certificate verifySourceStampCertificate( ByteBuffer sourceStampBlockData, byte[] sourceStampCertificateDigest) throws IOException { CertificateFactory certFactory; try { certFactory = CertificateFactory.getInstance("X.509"); } catch (CertificateException e) { throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); } // Parse the SourceStamp certificate. byte[] sourceStampEncodedCertificate = ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData); X509Certificate sourceStampCertificate; try { sourceStampCertificate = (X509Certificate) certFactory.generateCertificate( new ByteArrayInputStream(sourceStampEncodedCertificate)); } catch (CertificateException e) { throw new SecurityException("Failed to decode certificate", e); } byte[] sourceStampBlockCertificateDigest = computeSha256Digest(sourceStampEncodedCertificate); if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { throw new SecurityException("Certificate mismatch between APK and signature block"); } return new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate); } /** * Verify the SourceStamp signature found in the signing block is signed by the SourceStamp * certificate found in the APK. * * @param data the digest to be verified being signed by the source stamp * certificate. * @param sourceStampCertificate the source stamp certificate used to sign the stamp digests. * @param signatures the source stamp block in the APK signing block which contains * the stamp signed digests. */ private static void verifySourceStampSignature(byte[] data, X509Certificate sourceStampCertificate, ByteBuffer signatures) throws IOException { // Parse the signatures block and identify supported signatures int signatureCount = 0; int bestSigAlgorithm = -1; byte[] bestSigAlgorithmSignatureBytes = null; while (signatures.hasRemaining()) { signatureCount++; try { ByteBuffer signature = getLengthPrefixedSlice(signatures); if (signature.remaining() < 8) { throw new SecurityException("Signature record too short"); } int sigAlgorithm = signature.getInt(); if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { continue; } if ((bestSigAlgorithm == -1) || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { bestSigAlgorithm = sigAlgorithm; bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); } } catch (IOException | BufferUnderflowException e) { throw new SecurityException( "Failed to parse signature record #" + signatureCount, e); } } if (bestSigAlgorithm == -1) { if (signatureCount == 0) { throw new SecurityException("No signatures found"); } else { throw new SecurityException("No supported signatures found"); } } // Verify signatures over digests using the SourceStamp's certificate. Pair signatureAlgorithmParams = getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm); String jcaSignatureAlgorithm = signatureAlgorithmParams.first; AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; PublicKey publicKey = sourceStampCertificate.getPublicKey(); boolean sigVerified; try { Signature sig = Signature.getInstance(jcaSignatureAlgorithm); sig.initVerify(publicKey); if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } sig.update(data); sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); } catch (InvalidKeyException | InvalidAlgorithmParameterException | SignatureException | NoSuchAlgorithmException e) { throw new SecurityException( "Failed to verify " + jcaSignatureAlgorithm + " signature", e); } if (!sigVerified) { throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); } } private static Map> getSignatureSchemeApkContentDigests( RandomAccessFile apk, byte[] manifestBytes) throws IOException { Map> signatureSchemeApkContentDigests = new HashMap<>(); // Retrieve APK content digests in V3 signing block. try { SignatureInfo v3SignatureInfo = ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID); signatureSchemeApkContentDigests.put( VERSION_APK_SIGNATURE_SCHEME_V3, getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock)); } catch (SignatureNotFoundException e) { // It's fine not to find a V3 signature. } // Retrieve APK content digests in V2 signing block. try { SignatureInfo v2SignatureInfo = ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID); signatureSchemeApkContentDigests.put( VERSION_APK_SIGNATURE_SCHEME_V2, getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock)); } catch (SignatureNotFoundException e) { // It's fine not to find a V2 signature. } // Retrieve manifest digest. if (manifestBytes != null) { Map jarSignatureSchemeApkContentDigests = new HashMap<>(); jarSignatureSchemeApkContentDigests.put( CONTENT_DIGEST_SHA256, computeSha256Digest(manifestBytes)); signatureSchemeApkContentDigests.put( VERSION_JAR_SIGNATURE_SCHEME, jarSignatureSchemeApkContentDigests); } return signatureSchemeApkContentDigests; } private static Map getApkContentDigestsFromSignatureBlock( ByteBuffer signatureBlock) throws IOException { Map apkContentDigests = new HashMap<>(); ByteBuffer signers = getLengthPrefixedSlice(signatureBlock); while (signers.hasRemaining()) { ByteBuffer signer = getLengthPrefixedSlice(signers); ByteBuffer signedData = getLengthPrefixedSlice(signer); ByteBuffer digests = getLengthPrefixedSlice(signedData); while (digests.hasRemaining()) { ByteBuffer digest = getLengthPrefixedSlice(digests); int sigAlgorithm = digest.getInt(); byte[] contentDigest = readLengthPrefixedByteArray(digest); int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm); apkContentDigests.put(digestAlgorithm, contentDigest); } } return apkContentDigests; } private static Map getSignatureSchemeDigests( Map> signatureSchemeApkContentDigests) { Map digests = new HashMap<>(); for (Map.Entry> signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) { List> apkDigests = getApkDigests(signatureSchemeApkContentDigest.getValue()); digests.put( signatureSchemeApkContentDigest.getKey(), encodeApkContentDigests(apkDigests)); } return digests; } private static List> getApkDigests( Map apkContentDigests) { List> digests = new ArrayList<>(); for (Map.Entry apkContentDigest : apkContentDigests.entrySet()) { digests.add(Pair.create(apkContentDigest.getKey(), apkContentDigest.getValue())); } digests.sort(Comparator.comparing(pair -> pair.first)); return digests; } private static byte[] getSourceStampCertificateDigest(StrictJarFile apkJar) throws IOException { ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME); if (zipEntry == null) { // SourceStamp certificate hash file not found, which means that there is not // SourceStamp present. return null; } return Streams.readFully(apkJar.getInputStream(zipEntry)); } private static byte[] getManifestBytes(StrictJarFile apkJar) throws IOException { ZipEntry zipEntry = apkJar.findEntry(JarFile.MANIFEST_NAME); if (zipEntry == null) { return null; } return Streams.readFully(apkJar.getInputStream(zipEntry)); } private static byte[] encodeApkContentDigests(List> apkContentDigests) { int resultSize = 0; for (Pair element : apkContentDigests) { resultSize += 12 + element.second.length; } ByteBuffer result = ByteBuffer.allocate(resultSize); result.order(ByteOrder.LITTLE_ENDIAN); for (Pair element : apkContentDigests) { byte[] second = element.second; result.putInt(8 + second.length); result.putInt(element.first); result.putInt(second.length); result.put(second); } return result.array(); } private static ApkSigningBlockUtils.VerifiedProofOfRotation verifySourceStampAttributes( ByteBuffer stampAttributeData, X509Certificate sourceStampCertificate) throws IOException { CertificateFactory certFactory; try { certFactory = CertificateFactory.getInstance("X.509"); } catch (CertificateException e) { throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); } ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData); ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = null; while (stampAttributes.hasRemaining()) { ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes); int id = attribute.getInt(); if (id == PROOF_OF_ROTATION_ATTR_ID) { if (verifiedProofOfRotation != null) { throw new SecurityException("Encountered multiple Proof-of-rotation records" + " when verifying source stamp signature"); } verifiedProofOfRotation = verifyProofOfRotationStruct(attribute, certFactory); // Make sure that the last certificate in the Proof-of-rotation record matches // the one used to sign this APK. try { if (verifiedProofOfRotation.certs.size() > 0 && !Arrays.equals(verifiedProofOfRotation.certs.get( verifiedProofOfRotation.certs.size() - 1).getEncoded(), sourceStampCertificate.getEncoded())) { throw new SecurityException("Terminal certificate in Proof-of-rotation" + " record does not match source stamp certificate"); } } catch (CertificateEncodingException e) { throw new SecurityException("Failed to encode certificate when comparing" + " Proof-of-rotation record and source stamp certificate", e); } } } return verifiedProofOfRotation; } private static byte[] computeSha256Digest(byte[] input) { try { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(input); return messageDigest.digest(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Failed to find SHA-256", e); } } private static void closeApkJar(StrictJarFile apkJar) { try { if (apkJar == null) { return; } apkJar.close(); } catch (IOException e) { Slog.e(TAG, "Could not close APK jar", e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy