1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.util.apk; 18 19 import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_SHA256; 20 import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm; 21 import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; 22 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm; 23 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; 24 import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm; 25 import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; 26 import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct; 27 28 import android.util.Pair; 29 import android.util.Slog; 30 import android.util.jar.StrictJarFile; 31 32 import libcore.io.Streams; 33 34 import java.io.ByteArrayInputStream; 35 import java.io.IOException; 36 import java.io.RandomAccessFile; 37 import java.nio.BufferUnderflowException; 38 import java.nio.ByteBuffer; 39 import java.nio.ByteOrder; 40 import java.security.InvalidAlgorithmParameterException; 41 import java.security.InvalidKeyException; 42 import java.security.MessageDigest; 43 import java.security.NoSuchAlgorithmException; 44 import java.security.PublicKey; 45 import java.security.Signature; 46 import java.security.SignatureException; 47 import java.security.cert.Certificate; 48 import java.security.cert.CertificateEncodingException; 49 import java.security.cert.CertificateException; 50 import java.security.cert.CertificateFactory; 51 import java.security.cert.X509Certificate; 52 import java.security.spec.AlgorithmParameterSpec; 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.Collections; 56 import java.util.Comparator; 57 import java.util.HashMap; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.jar.JarFile; 61 import java.util.zip.ZipEntry; 62 63 /** 64 * Source Stamp verifier. 65 * 66 * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution. 67 * 68 * <p>The stamp is part of the APK that is protected by the signing block. 69 * 70 * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing 71 * block. 72 * 73 * @hide for internal use only. 74 */ 75 public abstract class SourceStampVerifier { 76 77 private static final String TAG = "SourceStampVerifier"; 78 79 private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; 80 private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; 81 private static final int SOURCE_STAMP_BLOCK_ID = 0x6dff800d; 82 private static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7; 83 84 private static final int VERSION_JAR_SIGNATURE_SCHEME = 1; 85 private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; 86 private static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; 87 88 /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ 89 private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; 90 91 /** Hidden constructor to prevent instantiation. */ SourceStampVerifier()92 private SourceStampVerifier() { 93 } 94 95 /** Verifies SourceStamp present in a list of (split) APKs for the same app. */ verify(List<String> apkFiles)96 public static SourceStampVerificationResult verify(List<String> apkFiles) { 97 Certificate stampCertificate = null; 98 List<? extends Certificate> stampCertificateLineage = Collections.emptyList(); 99 for (String apkFile : apkFiles) { 100 SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile); 101 if (!sourceStampVerificationResult.isPresent() 102 || !sourceStampVerificationResult.isVerified()) { 103 return sourceStampVerificationResult; 104 } 105 if (stampCertificate != null 106 && (!stampCertificate.equals(sourceStampVerificationResult.getCertificate()) 107 || !stampCertificateLineage.equals( 108 sourceStampVerificationResult.getCertificateLineage()))) { 109 return SourceStampVerificationResult.notVerified(); 110 } 111 stampCertificate = sourceStampVerificationResult.getCertificate(); 112 stampCertificateLineage = sourceStampVerificationResult.getCertificateLineage(); 113 } 114 return SourceStampVerificationResult.verified(stampCertificate, stampCertificateLineage); 115 } 116 117 /** Verifies SourceStamp present in the provided APK. */ verify(String apkFile)118 public static SourceStampVerificationResult verify(String apkFile) { 119 StrictJarFile apkJar = null; 120 try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { 121 apkJar = 122 new StrictJarFile( 123 apkFile, 124 /* verify= */ false, 125 /* signatureSchemeRollbackProtectionsEnforced= */ false); 126 byte[] sourceStampCertificateDigest = getSourceStampCertificateDigest(apkJar); 127 if (sourceStampCertificateDigest == null) { 128 // SourceStamp certificate hash file not found, which means that there is not 129 // SourceStamp present. 130 return SourceStampVerificationResult.notPresent(); 131 } 132 byte[] manifestBytes = getManifestBytes(apkJar); 133 return verify(apk, sourceStampCertificateDigest, manifestBytes); 134 } catch (IOException e) { 135 // Any exception in reading the APK returns a non-present SourceStamp outcome 136 // without affecting the outcome of any of the other signature schemes. 137 return SourceStampVerificationResult.notPresent(); 138 } finally { 139 closeApkJar(apkJar); 140 } 141 } 142 verify( RandomAccessFile apk, byte[] sourceStampCertificateDigest, byte[] manifestBytes)143 private static SourceStampVerificationResult verify( 144 RandomAccessFile apk, byte[] sourceStampCertificateDigest, byte[] manifestBytes) { 145 try { 146 SignatureInfo signatureInfo = 147 ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID); 148 Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests = 149 getSignatureSchemeApkContentDigests(apk, manifestBytes); 150 return verify( 151 signatureInfo, 152 getSignatureSchemeDigests(signatureSchemeApkContentDigests), 153 sourceStampCertificateDigest); 154 } catch (IOException | SignatureNotFoundException | RuntimeException e) { 155 return SourceStampVerificationResult.notVerified(); 156 } 157 } 158 verify( SignatureInfo signatureInfo, Map<Integer, byte[]> signatureSchemeDigests, byte[] sourceStampCertificateDigest)159 private static SourceStampVerificationResult verify( 160 SignatureInfo signatureInfo, 161 Map<Integer, byte[]> signatureSchemeDigests, 162 byte[] sourceStampCertificateDigest) 163 throws SecurityException, IOException { 164 ByteBuffer sourceStampBlock = signatureInfo.signatureBlock; 165 ByteBuffer sourceStampBlockData = 166 ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); 167 168 X509Certificate sourceStampCertificate = 169 verifySourceStampCertificate(sourceStampBlockData, sourceStampCertificateDigest); 170 171 // Parse signed signature schemes block. 172 ByteBuffer signedSignatureSchemes = 173 ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); 174 Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>(); 175 while (signedSignatureSchemes.hasRemaining()) { 176 ByteBuffer signedSignatureScheme = 177 ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes); 178 int signatureSchemeId = signedSignatureScheme.getInt(); 179 signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme); 180 } 181 182 for (Map.Entry<Integer, byte[]> signatureSchemeDigest : signatureSchemeDigests.entrySet()) { 183 if (!signedSignatureSchemeData.containsKey(signatureSchemeDigest.getKey())) { 184 throw new SecurityException( 185 String.format( 186 "No signatures found for signature scheme %d", 187 signatureSchemeDigest.getKey())); 188 } 189 ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice( 190 signedSignatureSchemeData.get(signatureSchemeDigest.getKey())); 191 verifySourceStampSignature( 192 signatureSchemeDigest.getValue(), 193 sourceStampCertificate, 194 signatures); 195 } 196 197 List<? extends Certificate> sourceStampCertificateLineage = Collections.emptyList(); 198 if (sourceStampBlockData.hasRemaining()) { 199 // The stamp block contains some additional attributes. 200 ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData); 201 ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData); 202 203 byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()]; 204 stampAttributeData.get(stampAttributeBytes); 205 stampAttributeData.flip(); 206 207 verifySourceStampSignature(stampAttributeBytes, sourceStampCertificate, 208 stampAttributeDataSignatures); 209 ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = 210 verifySourceStampAttributes(stampAttributeData, sourceStampCertificate); 211 if (verifiedProofOfRotation != null) { 212 sourceStampCertificateLineage = verifiedProofOfRotation.certs; 213 } 214 } 215 216 return SourceStampVerificationResult.verified(sourceStampCertificate, 217 sourceStampCertificateLineage); 218 } 219 220 /** 221 * Verify the SourceStamp certificate found in the signing block is the same as the SourceStamp 222 * certificate found in the APK. It returns the verified certificate. 223 * 224 * @param sourceStampBlockData the source stamp block in the APK signing block which 225 * contains 226 * the certificate used to sign the stamp digests. 227 * @param sourceStampCertificateDigest the source stamp certificate digest found in the APK. 228 */ verifySourceStampCertificate( ByteBuffer sourceStampBlockData, byte[] sourceStampCertificateDigest)229 private static X509Certificate verifySourceStampCertificate( 230 ByteBuffer sourceStampBlockData, byte[] sourceStampCertificateDigest) 231 throws IOException { 232 CertificateFactory certFactory; 233 try { 234 certFactory = CertificateFactory.getInstance("X.509"); 235 } catch (CertificateException e) { 236 throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); 237 } 238 239 // Parse the SourceStamp certificate. 240 byte[] sourceStampEncodedCertificate = 241 ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData); 242 X509Certificate sourceStampCertificate; 243 try { 244 sourceStampCertificate = 245 (X509Certificate) 246 certFactory.generateCertificate( 247 new ByteArrayInputStream(sourceStampEncodedCertificate)); 248 } catch (CertificateException e) { 249 throw new SecurityException("Failed to decode certificate", e); 250 } 251 252 byte[] sourceStampBlockCertificateDigest = 253 computeSha256Digest(sourceStampEncodedCertificate); 254 if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { 255 throw new SecurityException("Certificate mismatch between APK and signature block"); 256 } 257 258 return new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate); 259 } 260 261 /** 262 * Verify the SourceStamp signature found in the signing block is signed by the SourceStamp 263 * certificate found in the APK. 264 * 265 * @param data the digest to be verified being signed by the source stamp 266 * certificate. 267 * @param sourceStampCertificate the source stamp certificate used to sign the stamp digests. 268 * @param signatures the source stamp block in the APK signing block which contains 269 * the stamp signed digests. 270 */ verifySourceStampSignature(byte[] data, X509Certificate sourceStampCertificate, ByteBuffer signatures)271 private static void verifySourceStampSignature(byte[] data, 272 X509Certificate sourceStampCertificate, ByteBuffer signatures) 273 throws IOException { 274 // Parse the signatures block and identify supported signatures 275 int signatureCount = 0; 276 int bestSigAlgorithm = -1; 277 byte[] bestSigAlgorithmSignatureBytes = null; 278 while (signatures.hasRemaining()) { 279 signatureCount++; 280 try { 281 ByteBuffer signature = getLengthPrefixedSlice(signatures); 282 if (signature.remaining() < 8) { 283 throw new SecurityException("Signature record too short"); 284 } 285 int sigAlgorithm = signature.getInt(); 286 if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { 287 continue; 288 } 289 if ((bestSigAlgorithm == -1) 290 || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { 291 bestSigAlgorithm = sigAlgorithm; 292 bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); 293 } 294 } catch (IOException | BufferUnderflowException e) { 295 throw new SecurityException( 296 "Failed to parse signature record #" + signatureCount, e); 297 } 298 } 299 if (bestSigAlgorithm == -1) { 300 if (signatureCount == 0) { 301 throw new SecurityException("No signatures found"); 302 } else { 303 throw new SecurityException("No supported signatures found"); 304 } 305 } 306 307 // Verify signatures over digests using the SourceStamp's certificate. 308 Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams = 309 getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm); 310 String jcaSignatureAlgorithm = signatureAlgorithmParams.first; 311 AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; 312 PublicKey publicKey = sourceStampCertificate.getPublicKey(); 313 boolean sigVerified; 314 try { 315 Signature sig = Signature.getInstance(jcaSignatureAlgorithm); 316 sig.initVerify(publicKey); 317 if (jcaSignatureAlgorithmParams != null) { 318 sig.setParameter(jcaSignatureAlgorithmParams); 319 } 320 sig.update(data); 321 sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); 322 } catch (InvalidKeyException 323 | InvalidAlgorithmParameterException 324 | SignatureException 325 | NoSuchAlgorithmException e) { 326 throw new SecurityException( 327 "Failed to verify " + jcaSignatureAlgorithm + " signature", e); 328 } 329 if (!sigVerified) { 330 throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); 331 } 332 } 333 getSignatureSchemeApkContentDigests( RandomAccessFile apk, byte[] manifestBytes)334 private static Map<Integer, Map<Integer, byte[]>> getSignatureSchemeApkContentDigests( 335 RandomAccessFile apk, byte[] manifestBytes) throws IOException { 336 Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests = new HashMap<>(); 337 338 // Retrieve APK content digests in V3 signing block. 339 try { 340 SignatureInfo v3SignatureInfo = 341 ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID); 342 signatureSchemeApkContentDigests.put( 343 VERSION_APK_SIGNATURE_SCHEME_V3, 344 getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock)); 345 } catch (SignatureNotFoundException e) { 346 // It's fine not to find a V3 signature. 347 } 348 349 // Retrieve APK content digests in V2 signing block. 350 try { 351 SignatureInfo v2SignatureInfo = 352 ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID); 353 signatureSchemeApkContentDigests.put( 354 VERSION_APK_SIGNATURE_SCHEME_V2, 355 getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock)); 356 } catch (SignatureNotFoundException e) { 357 // It's fine not to find a V2 signature. 358 } 359 360 // Retrieve manifest digest. 361 if (manifestBytes != null) { 362 Map<Integer, byte[]> jarSignatureSchemeApkContentDigests = new HashMap<>(); 363 jarSignatureSchemeApkContentDigests.put( 364 CONTENT_DIGEST_SHA256, computeSha256Digest(manifestBytes)); 365 signatureSchemeApkContentDigests.put( 366 VERSION_JAR_SIGNATURE_SCHEME, jarSignatureSchemeApkContentDigests); 367 } 368 369 return signatureSchemeApkContentDigests; 370 } 371 getApkContentDigestsFromSignatureBlock( ByteBuffer signatureBlock)372 private static Map<Integer, byte[]> getApkContentDigestsFromSignatureBlock( 373 ByteBuffer signatureBlock) throws IOException { 374 Map<Integer, byte[]> apkContentDigests = new HashMap<>(); 375 ByteBuffer signers = getLengthPrefixedSlice(signatureBlock); 376 while (signers.hasRemaining()) { 377 ByteBuffer signer = getLengthPrefixedSlice(signers); 378 ByteBuffer signedData = getLengthPrefixedSlice(signer); 379 ByteBuffer digests = getLengthPrefixedSlice(signedData); 380 while (digests.hasRemaining()) { 381 ByteBuffer digest = getLengthPrefixedSlice(digests); 382 int sigAlgorithm = digest.getInt(); 383 byte[] contentDigest = readLengthPrefixedByteArray(digest); 384 int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm); 385 apkContentDigests.put(digestAlgorithm, contentDigest); 386 } 387 } 388 return apkContentDigests; 389 } 390 getSignatureSchemeDigests( Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests)391 private static Map<Integer, byte[]> getSignatureSchemeDigests( 392 Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests) { 393 Map<Integer, byte[]> digests = new HashMap<>(); 394 for (Map.Entry<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigest : 395 signatureSchemeApkContentDigests.entrySet()) { 396 List<Pair<Integer, byte[]>> apkDigests = 397 getApkDigests(signatureSchemeApkContentDigest.getValue()); 398 digests.put( 399 signatureSchemeApkContentDigest.getKey(), encodeApkContentDigests(apkDigests)); 400 } 401 return digests; 402 } 403 getApkDigests( Map<Integer, byte[]> apkContentDigests)404 private static List<Pair<Integer, byte[]>> getApkDigests( 405 Map<Integer, byte[]> apkContentDigests) { 406 List<Pair<Integer, byte[]>> digests = new ArrayList<>(); 407 for (Map.Entry<Integer, byte[]> apkContentDigest : apkContentDigests.entrySet()) { 408 digests.add(Pair.create(apkContentDigest.getKey(), apkContentDigest.getValue())); 409 } 410 digests.sort(Comparator.comparing(pair -> pair.first)); 411 return digests; 412 } 413 getSourceStampCertificateDigest(StrictJarFile apkJar)414 private static byte[] getSourceStampCertificateDigest(StrictJarFile apkJar) throws IOException { 415 ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME); 416 if (zipEntry == null) { 417 // SourceStamp certificate hash file not found, which means that there is not 418 // SourceStamp present. 419 return null; 420 } 421 return Streams.readFully(apkJar.getInputStream(zipEntry)); 422 } 423 getManifestBytes(StrictJarFile apkJar)424 private static byte[] getManifestBytes(StrictJarFile apkJar) throws IOException { 425 ZipEntry zipEntry = apkJar.findEntry(JarFile.MANIFEST_NAME); 426 if (zipEntry == null) { 427 return null; 428 } 429 return Streams.readFully(apkJar.getInputStream(zipEntry)); 430 } 431 encodeApkContentDigests(List<Pair<Integer, byte[]>> apkContentDigests)432 private static byte[] encodeApkContentDigests(List<Pair<Integer, byte[]>> apkContentDigests) { 433 int resultSize = 0; 434 for (Pair<Integer, byte[]> element : apkContentDigests) { 435 resultSize += 12 + element.second.length; 436 } 437 ByteBuffer result = ByteBuffer.allocate(resultSize); 438 result.order(ByteOrder.LITTLE_ENDIAN); 439 for (Pair<Integer, byte[]> element : apkContentDigests) { 440 byte[] second = element.second; 441 result.putInt(8 + second.length); 442 result.putInt(element.first); 443 result.putInt(second.length); 444 result.put(second); 445 } 446 return result.array(); 447 } 448 verifySourceStampAttributes( ByteBuffer stampAttributeData, X509Certificate sourceStampCertificate)449 private static ApkSigningBlockUtils.VerifiedProofOfRotation verifySourceStampAttributes( 450 ByteBuffer stampAttributeData, 451 X509Certificate sourceStampCertificate) 452 throws IOException { 453 CertificateFactory certFactory; 454 try { 455 certFactory = CertificateFactory.getInstance("X.509"); 456 } catch (CertificateException e) { 457 throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); 458 } 459 ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData); 460 ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = null; 461 while (stampAttributes.hasRemaining()) { 462 ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes); 463 int id = attribute.getInt(); 464 if (id == PROOF_OF_ROTATION_ATTR_ID) { 465 if (verifiedProofOfRotation != null) { 466 throw new SecurityException("Encountered multiple Proof-of-rotation records" 467 + " when verifying source stamp signature"); 468 } 469 verifiedProofOfRotation = verifyProofOfRotationStruct(attribute, certFactory); 470 // Make sure that the last certificate in the Proof-of-rotation record matches 471 // the one used to sign this APK. 472 try { 473 if (verifiedProofOfRotation.certs.size() > 0 474 && !Arrays.equals(verifiedProofOfRotation.certs.get( 475 verifiedProofOfRotation.certs.size() - 1).getEncoded(), 476 sourceStampCertificate.getEncoded())) { 477 throw new SecurityException("Terminal certificate in Proof-of-rotation" 478 + " record does not match source stamp certificate"); 479 } 480 } catch (CertificateEncodingException e) { 481 throw new SecurityException("Failed to encode certificate when comparing" 482 + " Proof-of-rotation record and source stamp certificate", e); 483 } 484 } 485 } 486 return verifiedProofOfRotation; 487 } 488 computeSha256Digest(byte[] input)489 private static byte[] computeSha256Digest(byte[] input) { 490 try { 491 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 492 messageDigest.update(input); 493 return messageDigest.digest(); 494 } catch (NoSuchAlgorithmException e) { 495 throw new RuntimeException("Failed to find SHA-256", e); 496 } 497 } 498 closeApkJar(StrictJarFile apkJar)499 private static void closeApkJar(StrictJarFile apkJar) { 500 try { 501 if (apkJar == null) { 502 return; 503 } 504 apkJar.close(); 505 } catch (IOException e) { 506 Slog.e(TAG, "Could not close APK jar", e); 507 } 508 } 509 } 510