| /* |
| * 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. |
| * |
| * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution. |
| * |
| * <p>The stamp is part of the APK that is protected by the signing block. |
| * |
| * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing |
| * block. |
| * |
| * @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<String> apkFiles) { |
| Certificate stampCertificate = null; |
| List<? extends Certificate> 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) { |
| SignatureInfo signatureInfo; |
| try { |
| signatureInfo = |
| ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID); |
| } catch (IOException | SignatureNotFoundException | RuntimeException e) { |
| return SourceStampVerificationResult.notPresent(); |
| } |
| try { |
| Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests = |
| getSignatureSchemeApkContentDigests(apk, manifestBytes); |
| return verify( |
| signatureInfo, |
| getSignatureSchemeDigests(signatureSchemeApkContentDigests), |
| sourceStampCertificateDigest); |
| } catch (IOException | RuntimeException e) { |
| return SourceStampVerificationResult.notVerified(); |
| } |
| } |
| |
| private static SourceStampVerificationResult verify( |
| SignatureInfo signatureInfo, |
| Map<Integer, byte[]> 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<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>(); |
| while (signedSignatureSchemes.hasRemaining()) { |
| ByteBuffer signedSignatureScheme = |
| ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes); |
| int signatureSchemeId = signedSignatureScheme.getInt(); |
| signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme); |
| } |
| |
| for (Map.Entry<Integer, byte[]> 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<? extends Certificate> 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<String, ? extends AlgorithmParameterSpec> 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<Integer, Map<Integer, byte[]>> getSignatureSchemeApkContentDigests( |
| RandomAccessFile apk, byte[] manifestBytes) throws IOException { |
| Map<Integer, Map<Integer, byte[]>> 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<Integer, byte[]> jarSignatureSchemeApkContentDigests = new HashMap<>(); |
| jarSignatureSchemeApkContentDigests.put( |
| CONTENT_DIGEST_SHA256, computeSha256Digest(manifestBytes)); |
| signatureSchemeApkContentDigests.put( |
| VERSION_JAR_SIGNATURE_SCHEME, jarSignatureSchemeApkContentDigests); |
| } |
| |
| return signatureSchemeApkContentDigests; |
| } |
| |
| private static Map<Integer, byte[]> getApkContentDigestsFromSignatureBlock( |
| ByteBuffer signatureBlock) throws IOException { |
| Map<Integer, byte[]> 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<Integer, byte[]> getSignatureSchemeDigests( |
| Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests) { |
| Map<Integer, byte[]> digests = new HashMap<>(); |
| for (Map.Entry<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigest : |
| signatureSchemeApkContentDigests.entrySet()) { |
| List<Pair<Integer, byte[]>> apkDigests = |
| getApkDigests(signatureSchemeApkContentDigest.getValue()); |
| digests.put( |
| signatureSchemeApkContentDigest.getKey(), encodeApkContentDigests(apkDigests)); |
| } |
| return digests; |
| } |
| |
| private static List<Pair<Integer, byte[]>> getApkDigests( |
| Map<Integer, byte[]> apkContentDigests) { |
| List<Pair<Integer, byte[]>> digests = new ArrayList<>(); |
| for (Map.Entry<Integer, byte[]> 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<Pair<Integer, byte[]>> apkContentDigests) { |
| int resultSize = 0; |
| for (Pair<Integer, byte[]> element : apkContentDigests) { |
| resultSize += 12 + element.second.length; |
| } |
| ByteBuffer result = ByteBuffer.allocate(resultSize); |
| result.order(ByteOrder.LITTLE_ENDIAN); |
| for (Pair<Integer, byte[]> 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); |
| } |
| } |
| } |