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.content.integrity; 18 19 import static com.android.internal.util.Preconditions.checkArgument; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import java.lang.annotation.Retention; 30 import java.lang.annotation.RetentionPolicy; 31 import java.nio.charset.StandardCharsets; 32 import java.security.MessageDigest; 33 import java.security.NoSuchAlgorithmException; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Objects; 37 38 /** 39 * Represents a simple formula consisting of an app install metadata field and a value. 40 * 41 * <p>Instances of this class are immutable. 42 * 43 * @hide 44 */ 45 @VisibleForTesting 46 public abstract class AtomicFormula extends IntegrityFormula { 47 48 /** @hide */ 49 @IntDef( 50 value = { 51 PACKAGE_NAME, 52 APP_CERTIFICATE, 53 INSTALLER_NAME, 54 INSTALLER_CERTIFICATE, 55 VERSION_CODE, 56 PRE_INSTALLED, 57 STAMP_TRUSTED, 58 STAMP_CERTIFICATE_HASH, 59 APP_CERTIFICATE_LINEAGE, 60 }) 61 @Retention(RetentionPolicy.SOURCE) 62 public @interface Key {} 63 64 /** @hide */ 65 @IntDef(value = {EQ, GT, GTE}) 66 @Retention(RetentionPolicy.SOURCE) 67 public @interface Operator {} 68 69 /** 70 * Package name of the app. 71 * 72 * <p>Can only be used in {@link StringAtomicFormula}. 73 */ 74 public static final int PACKAGE_NAME = 0; 75 76 /** 77 * SHA-256 of the app certificate of the app. 78 * 79 * <p>Can only be used in {@link StringAtomicFormula}. 80 */ 81 public static final int APP_CERTIFICATE = 1; 82 83 /** 84 * Package name of the installer. Will be empty string if installed by the system (e.g., adb). 85 * 86 * <p>Can only be used in {@link StringAtomicFormula}. 87 */ 88 public static final int INSTALLER_NAME = 2; 89 90 /** 91 * SHA-256 of the cert of the installer. Will be empty string if installed by the system (e.g., 92 * adb). 93 * 94 * <p>Can only be used in {@link StringAtomicFormula}. 95 */ 96 public static final int INSTALLER_CERTIFICATE = 3; 97 98 /** 99 * Version code of the app. 100 * 101 * <p>Can only be used in {@link LongAtomicFormula}. 102 */ 103 public static final int VERSION_CODE = 4; 104 105 /** 106 * If the app is pre-installed on the device. 107 * 108 * <p>Can only be used in {@link BooleanAtomicFormula}. 109 */ 110 public static final int PRE_INSTALLED = 5; 111 112 /** 113 * If the APK has an embedded trusted stamp. 114 * 115 * <p>Can only be used in {@link BooleanAtomicFormula}. 116 */ 117 public static final int STAMP_TRUSTED = 6; 118 119 /** 120 * SHA-256 of the certificate used to sign the stamp embedded in the APK. 121 * 122 * <p>Can only be used in {@link StringAtomicFormula}. 123 */ 124 public static final int STAMP_CERTIFICATE_HASH = 7; 125 126 /** 127 * SHA-256 of a certificate in the signing lineage of the app. 128 * 129 * <p>Can only be used in {@link StringAtomicFormula}. 130 */ 131 public static final int APP_CERTIFICATE_LINEAGE = 8; 132 133 public static final int EQ = 0; 134 public static final int GT = 1; 135 public static final int GTE = 2; 136 137 private final @Key int mKey; 138 AtomicFormula(@ey int key)139 public AtomicFormula(@Key int key) { 140 checkArgument(isValidKey(key), "Unknown key: %d", key); 141 mKey = key; 142 } 143 144 /** An {@link AtomicFormula} with an key and long value. */ 145 public static final class LongAtomicFormula extends AtomicFormula implements Parcelable { 146 private final Long mValue; 147 private final @Operator Integer mOperator; 148 149 /** 150 * Constructs an empty {@link LongAtomicFormula}. This should only be used as a base. 151 * 152 * <p>This formula will always return false. 153 * 154 * @throws IllegalArgumentException if {@code key} cannot be used with long value 155 */ LongAtomicFormula(@ey int key)156 public LongAtomicFormula(@Key int key) { 157 super(key); 158 checkArgument( 159 key == VERSION_CODE, 160 "Key %s cannot be used with LongAtomicFormula", keyToString(key)); 161 mValue = null; 162 mOperator = null; 163 } 164 165 /** 166 * Constructs a new {@link LongAtomicFormula}. 167 * 168 * <p>This formula will hold if and only if the corresponding information of an install 169 * specified by {@code key} is of the correct relationship to {@code value} as specified by 170 * {@code operator}. 171 * 172 * @throws IllegalArgumentException if {@code key} cannot be used with long value 173 */ LongAtomicFormula(@ey int key, @Operator int operator, long value)174 public LongAtomicFormula(@Key int key, @Operator int operator, long value) { 175 super(key); 176 checkArgument( 177 key == VERSION_CODE, 178 "Key %s cannot be used with LongAtomicFormula", keyToString(key)); 179 checkArgument( 180 isValidOperator(operator), "Unknown operator: %d", operator); 181 mOperator = operator; 182 mValue = value; 183 } 184 LongAtomicFormula(Parcel in)185 LongAtomicFormula(Parcel in) { 186 super(in.readInt()); 187 mValue = in.readLong(); 188 mOperator = in.readInt(); 189 } 190 191 @NonNull 192 public static final Creator<LongAtomicFormula> CREATOR = 193 new Creator<LongAtomicFormula>() { 194 @Override 195 public LongAtomicFormula createFromParcel(Parcel in) { 196 return new LongAtomicFormula(in); 197 } 198 199 @Override 200 public LongAtomicFormula[] newArray(int size) { 201 return new LongAtomicFormula[size]; 202 } 203 }; 204 205 @Override getTag()206 public int getTag() { 207 return IntegrityFormula.LONG_ATOMIC_FORMULA_TAG; 208 } 209 210 @Override matches(AppInstallMetadata appInstallMetadata)211 public boolean matches(AppInstallMetadata appInstallMetadata) { 212 if (mValue == null || mOperator == null) { 213 return false; 214 } 215 216 long metadataValue = getLongMetadataValue(appInstallMetadata, getKey()); 217 switch (mOperator) { 218 case EQ: 219 return metadataValue == mValue; 220 case GT: 221 return metadataValue > mValue; 222 case GTE: 223 return metadataValue >= mValue; 224 default: 225 throw new IllegalArgumentException( 226 String.format("Unexpected operator %d", mOperator)); 227 } 228 } 229 230 @Override isAppCertificateFormula()231 public boolean isAppCertificateFormula() { 232 return false; 233 } 234 235 @Override isAppCertificateLineageFormula()236 public boolean isAppCertificateLineageFormula() { 237 return false; 238 } 239 240 @Override isInstallerFormula()241 public boolean isInstallerFormula() { 242 return false; 243 } 244 245 @Override toString()246 public String toString() { 247 if (mValue == null || mOperator == null) { 248 return String.format("(%s)", keyToString(getKey())); 249 } 250 return String.format( 251 "(%s %s %s)", keyToString(getKey()), operatorToString(mOperator), mValue); 252 } 253 254 @Override equals(@ullable Object o)255 public boolean equals(@Nullable Object o) { 256 if (this == o) { 257 return true; 258 } 259 if (o == null || getClass() != o.getClass()) { 260 return false; 261 } 262 LongAtomicFormula that = (LongAtomicFormula) o; 263 return getKey() == that.getKey() 264 && Objects.equals(mValue, that.mValue) 265 && Objects.equals(mOperator, that.mOperator); 266 } 267 268 @Override hashCode()269 public int hashCode() { 270 return Objects.hash(getKey(), mOperator, mValue); 271 } 272 273 @Override describeContents()274 public int describeContents() { 275 return 0; 276 } 277 278 @Override writeToParcel(@onNull Parcel dest, int flags)279 public void writeToParcel(@NonNull Parcel dest, int flags) { 280 if (mValue == null || mOperator == null) { 281 throw new IllegalStateException("Cannot write an empty LongAtomicFormula."); 282 } 283 dest.writeInt(getKey()); 284 dest.writeLong(mValue); 285 dest.writeInt(mOperator); 286 } 287 getValue()288 public Long getValue() { 289 return mValue; 290 } 291 getOperator()292 public Integer getOperator() { 293 return mOperator; 294 } 295 isValidOperator(int operator)296 private static boolean isValidOperator(int operator) { 297 return operator == EQ || operator == GT || operator == GTE; 298 } 299 getLongMetadataValue(AppInstallMetadata appInstallMetadata, int key)300 private static long getLongMetadataValue(AppInstallMetadata appInstallMetadata, int key) { 301 switch (key) { 302 case AtomicFormula.VERSION_CODE: 303 return appInstallMetadata.getVersionCode(); 304 default: 305 throw new IllegalStateException("Unexpected key in IntAtomicFormula" + key); 306 } 307 } 308 } 309 310 /** An {@link AtomicFormula} with a key and string value. */ 311 public static final class StringAtomicFormula extends AtomicFormula implements Parcelable { 312 private final String mValue; 313 // Indicates whether the value is the actual value or the hashed value. 314 private final Boolean mIsHashedValue; 315 316 /** 317 * Constructs an empty {@link StringAtomicFormula}. This should only be used as a base. 318 * 319 * <p>An empty formula will always match to false. 320 * 321 * @throws IllegalArgumentException if {@code key} cannot be used with string value 322 */ StringAtomicFormula(@ey int key)323 public StringAtomicFormula(@Key int key) { 324 super(key); 325 checkArgument( 326 key == PACKAGE_NAME 327 || key == APP_CERTIFICATE 328 || key == INSTALLER_CERTIFICATE 329 || key == INSTALLER_NAME 330 || key == STAMP_CERTIFICATE_HASH 331 || key == APP_CERTIFICATE_LINEAGE, 332 "Key %s cannot be used with StringAtomicFormula", keyToString(key)); 333 mValue = null; 334 mIsHashedValue = null; 335 } 336 337 /** 338 * Constructs a new {@link StringAtomicFormula}. 339 * 340 * <p>This formula will hold if and only if the corresponding information of an install 341 * specified by {@code key} equals {@code value}. 342 * 343 * @throws IllegalArgumentException if {@code key} cannot be used with string value 344 */ StringAtomicFormula(@ey int key, @NonNull String value, boolean isHashed)345 public StringAtomicFormula(@Key int key, @NonNull String value, boolean isHashed) { 346 super(key); 347 checkArgument( 348 key == PACKAGE_NAME 349 || key == APP_CERTIFICATE 350 || key == INSTALLER_CERTIFICATE 351 || key == INSTALLER_NAME 352 || key == STAMP_CERTIFICATE_HASH 353 || key == APP_CERTIFICATE_LINEAGE, 354 "Key %s cannot be used with StringAtomicFormula", keyToString(key)); 355 mValue = value; 356 mIsHashedValue = isHashed; 357 } 358 359 /** 360 * Constructs a new {@link StringAtomicFormula} together with handling the necessary hashing 361 * for the given key. 362 * 363 * <p>The value will be automatically hashed with SHA256 and the hex digest will be computed 364 * when the key is PACKAGE_NAME or INSTALLER_NAME and the value is more than 32 characters. 365 * 366 * <p>The APP_CERTIFICATES, INSTALLER_CERTIFICATES, STAMP_CERTIFICATE_HASH and 367 * APP_CERTIFICATE_LINEAGE are always delivered in hashed form. So the isHashedValue is set 368 * to true by default. 369 * 370 * @throws IllegalArgumentException if {@code key} cannot be used with string value. 371 */ StringAtomicFormula(@ey int key, @NonNull String value)372 public StringAtomicFormula(@Key int key, @NonNull String value) { 373 super(key); 374 checkArgument( 375 key == PACKAGE_NAME 376 || key == APP_CERTIFICATE 377 || key == INSTALLER_CERTIFICATE 378 || key == INSTALLER_NAME 379 || key == STAMP_CERTIFICATE_HASH 380 || key == APP_CERTIFICATE_LINEAGE, 381 "Key %s cannot be used with StringAtomicFormula", keyToString(key)); 382 mValue = hashValue(key, value); 383 mIsHashedValue = 384 (key == APP_CERTIFICATE 385 || key == INSTALLER_CERTIFICATE 386 || key == STAMP_CERTIFICATE_HASH 387 || key == APP_CERTIFICATE_LINEAGE) 388 || !mValue.equals(value); 389 } 390 StringAtomicFormula(Parcel in)391 StringAtomicFormula(Parcel in) { 392 super(in.readInt()); 393 mValue = in.readStringNoHelper(); 394 mIsHashedValue = in.readByte() != 0; 395 } 396 397 @NonNull 398 public static final Creator<StringAtomicFormula> CREATOR = 399 new Creator<StringAtomicFormula>() { 400 @Override 401 public StringAtomicFormula createFromParcel(Parcel in) { 402 return new StringAtomicFormula(in); 403 } 404 405 @Override 406 public StringAtomicFormula[] newArray(int size) { 407 return new StringAtomicFormula[size]; 408 } 409 }; 410 411 @Override getTag()412 public int getTag() { 413 return IntegrityFormula.STRING_ATOMIC_FORMULA_TAG; 414 } 415 416 @Override matches(AppInstallMetadata appInstallMetadata)417 public boolean matches(AppInstallMetadata appInstallMetadata) { 418 if (mValue == null || mIsHashedValue == null) { 419 return false; 420 } 421 return getMetadataValue(appInstallMetadata, getKey()).contains(mValue); 422 } 423 424 @Override isAppCertificateFormula()425 public boolean isAppCertificateFormula() { 426 return getKey() == APP_CERTIFICATE; 427 } 428 429 @Override isAppCertificateLineageFormula()430 public boolean isAppCertificateLineageFormula() { 431 return getKey() == APP_CERTIFICATE_LINEAGE; 432 } 433 434 @Override isInstallerFormula()435 public boolean isInstallerFormula() { 436 return getKey() == INSTALLER_NAME || getKey() == INSTALLER_CERTIFICATE; 437 } 438 439 @Override toString()440 public String toString() { 441 if (mValue == null || mIsHashedValue == null) { 442 return String.format("(%s)", keyToString(getKey())); 443 } 444 return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue); 445 } 446 447 @Override equals(@ullable Object o)448 public boolean equals(@Nullable Object o) { 449 if (this == o) { 450 return true; 451 } 452 if (o == null || getClass() != o.getClass()) { 453 return false; 454 } 455 StringAtomicFormula that = (StringAtomicFormula) o; 456 return getKey() == that.getKey() && Objects.equals(mValue, that.mValue); 457 } 458 459 @Override hashCode()460 public int hashCode() { 461 return Objects.hash(getKey(), mValue); 462 } 463 464 @Override describeContents()465 public int describeContents() { 466 return 0; 467 } 468 469 @Override writeToParcel(@onNull Parcel dest, int flags)470 public void writeToParcel(@NonNull Parcel dest, int flags) { 471 if (mValue == null || mIsHashedValue == null) { 472 throw new IllegalStateException("Cannot write an empty StringAtomicFormula."); 473 } 474 dest.writeInt(getKey()); 475 dest.writeStringNoHelper(mValue); 476 dest.writeByte((byte) (mIsHashedValue ? 1 : 0)); 477 } 478 getValue()479 public String getValue() { 480 return mValue; 481 } 482 getIsHashedValue()483 public Boolean getIsHashedValue() { 484 return mIsHashedValue; 485 } 486 getMetadataValue( AppInstallMetadata appInstallMetadata, int key)487 private static List<String> getMetadataValue( 488 AppInstallMetadata appInstallMetadata, int key) { 489 switch (key) { 490 case AtomicFormula.PACKAGE_NAME: 491 return Collections.singletonList(appInstallMetadata.getPackageName()); 492 case AtomicFormula.APP_CERTIFICATE: 493 return appInstallMetadata.getAppCertificates(); 494 case AtomicFormula.INSTALLER_CERTIFICATE: 495 return appInstallMetadata.getInstallerCertificates(); 496 case AtomicFormula.INSTALLER_NAME: 497 return Collections.singletonList(appInstallMetadata.getInstallerName()); 498 case AtomicFormula.STAMP_CERTIFICATE_HASH: 499 return Collections.singletonList(appInstallMetadata.getStampCertificateHash()); 500 case AtomicFormula.APP_CERTIFICATE_LINEAGE: 501 return appInstallMetadata.getAppCertificateLineage(); 502 default: 503 throw new IllegalStateException( 504 "Unexpected key in StringAtomicFormula: " + key); 505 } 506 } 507 hashValue(@ey int key, String value)508 private static String hashValue(@Key int key, String value) { 509 // Hash the string value if it is a PACKAGE_NAME or INSTALLER_NAME and the value is 510 // greater than 32 characters. 511 if (value.length() > 32) { 512 if (key == PACKAGE_NAME || key == INSTALLER_NAME) { 513 return hash(value); 514 } 515 } 516 return value; 517 } 518 hash(String value)519 private static String hash(String value) { 520 try { 521 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 522 byte[] hashBytes = messageDigest.digest(value.getBytes(StandardCharsets.UTF_8)); 523 return IntegrityUtils.getHexDigest(hashBytes); 524 } catch (NoSuchAlgorithmException e) { 525 throw new RuntimeException("SHA-256 algorithm not found", e); 526 } 527 } 528 } 529 530 /** An {@link AtomicFormula} with a key and boolean value. */ 531 public static final class BooleanAtomicFormula extends AtomicFormula implements Parcelable { 532 private final Boolean mValue; 533 534 /** 535 * Constructs an empty {@link BooleanAtomicFormula}. This should only be used as a base. 536 * 537 * <p>An empty formula will always match to false. 538 * 539 * @throws IllegalArgumentException if {@code key} cannot be used with boolean value 540 */ BooleanAtomicFormula(@ey int key)541 public BooleanAtomicFormula(@Key int key) { 542 super(key); 543 checkArgument( 544 key == PRE_INSTALLED || key == STAMP_TRUSTED, 545 String.format( 546 "Key %s cannot be used with BooleanAtomicFormula", keyToString(key))); 547 mValue = null; 548 } 549 550 /** 551 * Constructs a new {@link BooleanAtomicFormula}. 552 * 553 * <p>This formula will hold if and only if the corresponding information of an install 554 * specified by {@code key} equals {@code value}. 555 * 556 * @throws IllegalArgumentException if {@code key} cannot be used with boolean value 557 */ BooleanAtomicFormula(@ey int key, boolean value)558 public BooleanAtomicFormula(@Key int key, boolean value) { 559 super(key); 560 checkArgument( 561 key == PRE_INSTALLED || key == STAMP_TRUSTED, 562 String.format( 563 "Key %s cannot be used with BooleanAtomicFormula", keyToString(key))); 564 mValue = value; 565 } 566 BooleanAtomicFormula(Parcel in)567 BooleanAtomicFormula(Parcel in) { 568 super(in.readInt()); 569 mValue = in.readByte() != 0; 570 } 571 572 @NonNull 573 public static final Creator<BooleanAtomicFormula> CREATOR = 574 new Creator<BooleanAtomicFormula>() { 575 @Override 576 public BooleanAtomicFormula createFromParcel(Parcel in) { 577 return new BooleanAtomicFormula(in); 578 } 579 580 @Override 581 public BooleanAtomicFormula[] newArray(int size) { 582 return new BooleanAtomicFormula[size]; 583 } 584 }; 585 586 @Override getTag()587 public int getTag() { 588 return IntegrityFormula.BOOLEAN_ATOMIC_FORMULA_TAG; 589 } 590 591 @Override matches(AppInstallMetadata appInstallMetadata)592 public boolean matches(AppInstallMetadata appInstallMetadata) { 593 if (mValue == null) { 594 return false; 595 } 596 return getBooleanMetadataValue(appInstallMetadata, getKey()) == mValue; 597 } 598 599 @Override isAppCertificateFormula()600 public boolean isAppCertificateFormula() { 601 return false; 602 } 603 604 @Override isAppCertificateLineageFormula()605 public boolean isAppCertificateLineageFormula() { 606 return false; 607 } 608 609 @Override isInstallerFormula()610 public boolean isInstallerFormula() { 611 return false; 612 } 613 614 @Override toString()615 public String toString() { 616 if (mValue == null) { 617 return String.format("(%s)", keyToString(getKey())); 618 } 619 return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue); 620 } 621 622 @Override equals(@ullable Object o)623 public boolean equals(@Nullable Object o) { 624 if (this == o) { 625 return true; 626 } 627 if (o == null || getClass() != o.getClass()) { 628 return false; 629 } 630 BooleanAtomicFormula that = (BooleanAtomicFormula) o; 631 return getKey() == that.getKey() && Objects.equals(mValue, that.mValue); 632 } 633 634 @Override hashCode()635 public int hashCode() { 636 return Objects.hash(getKey(), mValue); 637 } 638 639 @Override describeContents()640 public int describeContents() { 641 return 0; 642 } 643 644 @Override writeToParcel(@onNull Parcel dest, int flags)645 public void writeToParcel(@NonNull Parcel dest, int flags) { 646 if (mValue == null) { 647 throw new IllegalStateException("Cannot write an empty BooleanAtomicFormula."); 648 } 649 dest.writeInt(getKey()); 650 dest.writeByte((byte) (mValue ? 1 : 0)); 651 } 652 getValue()653 public Boolean getValue() { 654 return mValue; 655 } 656 getBooleanMetadataValue( AppInstallMetadata appInstallMetadata, int key)657 private static boolean getBooleanMetadataValue( 658 AppInstallMetadata appInstallMetadata, int key) { 659 switch (key) { 660 case AtomicFormula.PRE_INSTALLED: 661 return appInstallMetadata.isPreInstalled(); 662 case AtomicFormula.STAMP_TRUSTED: 663 return appInstallMetadata.isStampTrusted(); 664 default: 665 throw new IllegalStateException( 666 "Unexpected key in BooleanAtomicFormula: " + key); 667 } 668 } 669 } 670 getKey()671 public int getKey() { 672 return mKey; 673 } 674 keyToString(int key)675 static String keyToString(int key) { 676 switch (key) { 677 case PACKAGE_NAME: 678 return "PACKAGE_NAME"; 679 case APP_CERTIFICATE: 680 return "APP_CERTIFICATE"; 681 case VERSION_CODE: 682 return "VERSION_CODE"; 683 case INSTALLER_NAME: 684 return "INSTALLER_NAME"; 685 case INSTALLER_CERTIFICATE: 686 return "INSTALLER_CERTIFICATE"; 687 case PRE_INSTALLED: 688 return "PRE_INSTALLED"; 689 case STAMP_TRUSTED: 690 return "STAMP_TRUSTED"; 691 case STAMP_CERTIFICATE_HASH: 692 return "STAMP_CERTIFICATE_HASH"; 693 case APP_CERTIFICATE_LINEAGE: 694 return "APP_CERTIFICATE_LINEAGE"; 695 default: 696 throw new IllegalArgumentException("Unknown key " + key); 697 } 698 } 699 operatorToString(int op)700 static String operatorToString(int op) { 701 switch (op) { 702 case EQ: 703 return "EQ"; 704 case GT: 705 return "GT"; 706 case GTE: 707 return "GTE"; 708 default: 709 throw new IllegalArgumentException("Unknown operator " + op); 710 } 711 } 712 isValidKey(int key)713 private static boolean isValidKey(int key) { 714 return key == PACKAGE_NAME 715 || key == APP_CERTIFICATE 716 || key == VERSION_CODE 717 || key == INSTALLER_NAME 718 || key == INSTALLER_CERTIFICATE 719 || key == PRE_INSTALLED 720 || key == STAMP_TRUSTED 721 || key == STAMP_CERTIFICATE_HASH 722 || key == APP_CERTIFICATE_LINEAGE; 723 } 724 } 725