| /* |
| * Copyright (C) 2022 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.server.security; |
| |
| import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE; |
| import static android.security.attestationverification.AttestationVerificationManager.PARAM_PUBLIC_KEY; |
| import static android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE; |
| import static android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS; |
| import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE; |
| import static android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY; |
| import static android.security.attestationverification.AttestationVerificationManager.localBindingTypeToString; |
| |
| import static com.android.server.security.AndroidKeystoreAttestationVerificationAttributes.VerifiedBootState.VERIFIED; |
| import static com.android.server.security.AndroidKeystoreAttestationVerificationAttributes.fromCertificate; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import android.annotation.NonNull; |
| import android.content.Context; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.security.attestationverification.AttestationVerificationManager.LocalBindingType; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import org.json.JSONObject; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.cert.CertPath; |
| import java.security.cert.CertPathValidator; |
| import java.security.cert.CertPathValidatorException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.PKIXCertPathChecker; |
| import java.security.cert.PKIXParameters; |
| import java.security.cert.TrustAnchor; |
| import java.security.cert.X509Certificate; |
| import java.time.LocalDate; |
| import java.time.ZoneId; |
| import java.time.temporal.ChronoUnit; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * Verifies Android key attestation according to the |
| * {@link android.security.attestationverification.AttestationVerificationManager#PROFILE_PEER_DEVICE PROFILE_PEER_DEVICE} |
| * profile. |
| * |
| * <p> |
| * The profile is satisfied by checking all the following: |
| * <ul> |
| * <li> TrustAnchor match |
| * <li> Certificate validity |
| * <li> Android OS 10 or higher |
| * <li> Hardware backed key store |
| * <li> Verified boot locked |
| * <li> Remote Patch level must be within 1 year of local patch if local patch is less than 1 year |
| * old. |
| * </ul> |
| * |
| * <p> |
| * Trust anchors are vendor-defined by populating |
| * {@link R.array#vendor_required_attestation_certificates} string array (defenined in |
| * {@code frameworks/base/core/res/res/values/vendor_required_attestation_certificates.xml}). |
| */ |
| class AttestationVerificationPeerDeviceVerifier { |
| private static final String TAG = "AVF"; |
| private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE); |
| private static final int MAX_PATCH_AGE_MONTHS = 12; |
| |
| /** |
| * Optional requirements bundle parameter key for {@code TYPE_PUBLIC_KEY} and |
| * {@code TYPE_CHALLENGE}. |
| * |
| * <p> |
| * This is NOT a part of the AVF API surface (neither public SDK nor internal to the |
| * system_server) and should really only be used by the CompanionDeviceManagerService (which |
| * duplicates the value rather than referencing it directly here). |
| */ |
| private static final String PARAM_OWNED_BY_SYSTEM = "android.key_owned_by_system"; |
| |
| private static final String ANDROID_SYSTEM_PACKAGE_NAME = "AndroidSystem"; |
| private static final Set<String> ANDROID_SYSTEM_PACKAGE_NAME_SET = |
| Collections.singleton(ANDROID_SYSTEM_PACKAGE_NAME); |
| |
| private final Context mContext; |
| private final Set<TrustAnchor> mTrustAnchors; |
| private final boolean mRevocationEnabled; |
| private final LocalDate mTestSystemDate; |
| private final LocalDate mTestLocalPatchDate; |
| private final CertificateFactory mCertificateFactory; |
| private final CertPathValidator mCertPathValidator; |
| |
| AttestationVerificationPeerDeviceVerifier(@NonNull Context context) throws Exception { |
| mContext = Objects.requireNonNull(context); |
| mCertificateFactory = CertificateFactory.getInstance("X.509"); |
| mCertPathValidator = CertPathValidator.getInstance("PKIX"); |
| mTrustAnchors = getTrustAnchors(); |
| mRevocationEnabled = true; |
| mTestSystemDate = null; |
| mTestLocalPatchDate = null; |
| } |
| |
| // Use ONLY for hermetic unit testing. |
| @VisibleForTesting |
| AttestationVerificationPeerDeviceVerifier(@NonNull Context context, |
| Set<TrustAnchor> trustAnchors, boolean revocationEnabled, |
| LocalDate systemDate, LocalDate localPatchDate) throws Exception { |
| mContext = Objects.requireNonNull(context); |
| mCertificateFactory = CertificateFactory.getInstance("X.509"); |
| mCertPathValidator = CertPathValidator.getInstance("PKIX"); |
| mTrustAnchors = trustAnchors; |
| mRevocationEnabled = revocationEnabled; |
| mTestSystemDate = systemDate; |
| mTestLocalPatchDate = localPatchDate; |
| } |
| |
| /** |
| * Verifies attestation for public key or challenge local binding. |
| * <p> |
| * The attestations must be suitable for {@link java.security.cert.CertificateFactory} |
| * The certificates in the attestation provided must be DER-encoded and may be supplied in |
| * binary or printable (Base64) encoding. If the certificate is provided in Base64 encoding, |
| * it must be bounded at the beginning by {@code -----BEGIN CERTIFICATE-----}, and must be |
| * bounded at the end by {@code -----END CERTIFICATE-----}. |
| * |
| * @param localBindingType Only {@code TYPE_PUBLIC_KEY} and {@code TYPE_CHALLENGE} supported. |
| * @param requirements Only {@code PARAM_PUBLIC_KEY} and {@code PARAM_CHALLENGE} supported. |
| * @param attestation Certificates should be DER encoded with leaf certificate appended first. |
| */ |
| int verifyAttestation( |
| @LocalBindingType int localBindingType, |
| @NonNull Bundle requirements, |
| @NonNull byte[] attestation) { |
| if (mCertificateFactory == null) { |
| debugVerboseLog("Unable to access CertificateFactory"); |
| return RESULT_FAILURE; |
| } |
| |
| if (mCertPathValidator == null) { |
| debugVerboseLog("Unable to access CertPathValidator"); |
| return RESULT_FAILURE; |
| } |
| |
| // Check if the provided local binding type is supported and if the provided requirements |
| // "match" the binding type. |
| if (!validateAttestationParameters(localBindingType, requirements)) { |
| return RESULT_FAILURE; |
| } |
| |
| try { |
| // First: parse and validate the certificate chain. |
| final List<X509Certificate> certificateChain = getCertificates(attestation); |
| // (returns void, but throws CertificateException and other similar Exceptions) |
| validateCertificateChain(certificateChain); |
| |
| final var leafCertificate = certificateChain.get(0); |
| final var attestationExtension = fromCertificate(leafCertificate); |
| |
| // Second: verify if the attestation satisfies the "peer device" profile. |
| if (!checkAttestationForPeerDeviceProfile(attestationExtension)) { |
| return RESULT_FAILURE; |
| } |
| |
| // Third: check if the attestation satisfies local binding requirements. |
| if (!checkLocalBindingRequirements( |
| leafCertificate, attestationExtension, localBindingType, requirements)) { |
| return RESULT_FAILURE; |
| } |
| |
| return RESULT_SUCCESS; |
| } catch (CertificateException | CertPathValidatorException |
| | InvalidAlgorithmParameterException | IOException e) { |
| // Catch all non-RuntimeExpceptions (all of these are thrown by either getCertificates() |
| // or validateCertificateChain() or |
| // AndroidKeystoreAttestationVerificationAttributes.fromCertificate()) |
| debugVerboseLog("Unable to parse/validate Android Attestation certificate(s)", e); |
| return RESULT_FAILURE; |
| } catch (RuntimeException e) { |
| // Catch everyting else (RuntimeExpcetions), since we don't want to throw any exceptions |
| // out of this class/method. |
| debugVerboseLog("Unexpected error", e); |
| return RESULT_FAILURE; |
| } |
| } |
| |
| @NonNull |
| private List<X509Certificate> getCertificates(byte[] attestation) |
| throws CertificateException { |
| List<X509Certificate> certificates = new ArrayList<>(); |
| ByteArrayInputStream bis = new ByteArrayInputStream(attestation); |
| while (bis.available() > 0) { |
| certificates.add((X509Certificate) mCertificateFactory.generateCertificate(bis)); |
| } |
| |
| return certificates; |
| } |
| |
| /** |
| * Check if the {@code localBindingType} is supported and if the {@code requirements} contains |
| * the required parameter for the given {@code @LocalBindingType}. |
| */ |
| private boolean validateAttestationParameters( |
| @LocalBindingType int localBindingType, @NonNull Bundle requirements) { |
| if (localBindingType != TYPE_PUBLIC_KEY && localBindingType != TYPE_CHALLENGE) { |
| debugVerboseLog("Binding type is not supported: " + localBindingType); |
| return false; |
| } |
| |
| if (requirements.size() < 1) { |
| debugVerboseLog("At least 1 requirement is required."); |
| return false; |
| } |
| |
| if (localBindingType == TYPE_PUBLIC_KEY && !requirements.containsKey(PARAM_PUBLIC_KEY)) { |
| debugVerboseLog("Requirements does not contain key: " + PARAM_PUBLIC_KEY); |
| return false; |
| } |
| |
| if (localBindingType == TYPE_CHALLENGE && !requirements.containsKey(PARAM_CHALLENGE)) { |
| debugVerboseLog("Requirements does not contain key: " + PARAM_CHALLENGE); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private void validateCertificateChain(List<X509Certificate> certificates) |
| throws CertificateException, CertPathValidatorException, |
| InvalidAlgorithmParameterException { |
| if (certificates.size() < 2) { |
| debugVerboseLog("Certificate chain less than 2 in size."); |
| throw new CertificateException("Certificate chain less than 2 in size."); |
| } |
| |
| CertPath certificatePath = mCertificateFactory.generateCertPath(certificates); |
| PKIXParameters validationParams = new PKIXParameters(mTrustAnchors); |
| if (mRevocationEnabled) { |
| // Checks Revocation Status List based on |
| // https://developer.android.com/training/articles/security-key-attestation#certificate_status |
| PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker(); |
| validationParams.addCertPathChecker(checker); |
| } |
| // Do not use built-in revocation status checker. |
| validationParams.setRevocationEnabled(false); |
| mCertPathValidator.validate(certificatePath, validationParams); |
| } |
| |
| private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException { |
| Set<TrustAnchor> modifiableSet = new HashSet<>(); |
| try { |
| for (String certString: getTrustAnchorResources()) { |
| modifiableSet.add( |
| new TrustAnchor((X509Certificate) mCertificateFactory.generateCertificate( |
| new ByteArrayInputStream(getCertificateBytes(certString))), null)); |
| } |
| } catch (CertificateException e) { |
| e.printStackTrace(); |
| throw new CertPathValidatorException("Invalid trust anchor certificate.", e); |
| } |
| return Collections.unmodifiableSet(modifiableSet); |
| } |
| |
| private byte[] getCertificateBytes(String certString) { |
| String formattedCertString = certString.replaceAll("\\s+", "\n"); |
| formattedCertString = formattedCertString.replaceAll( |
| "-BEGIN\\nCERTIFICATE-", "-BEGIN CERTIFICATE-"); |
| formattedCertString = formattedCertString.replaceAll( |
| "-END\\nCERTIFICATE-", "-END CERTIFICATE-"); |
| return formattedCertString.getBytes(UTF_8); |
| } |
| |
| private String[] getTrustAnchorResources() { |
| return mContext.getResources().getStringArray( |
| R.array.vendor_required_attestation_certificates); |
| } |
| |
| private boolean checkLocalBindingRequirements( |
| @NonNull X509Certificate leafCertificate, |
| @NonNull AndroidKeystoreAttestationVerificationAttributes attestationAttributes, |
| @LocalBindingType int localBindingType, |
| @NonNull Bundle requirements) { |
| // First: check non-optional (for the given local binding type) requirements. |
| switch (localBindingType) { |
| case TYPE_PUBLIC_KEY: |
| // Verify leaf public key matches provided public key. |
| final boolean publicKeyMatches = checkPublicKey( |
| leafCertificate, requirements.getByteArray(PARAM_PUBLIC_KEY)); |
| if (!publicKeyMatches) { |
| debugVerboseLog( |
| "Provided public key does not match leaf certificate public key."); |
| return false; |
| } |
| break; |
| |
| case TYPE_CHALLENGE: |
| // Verify challenge matches provided challenge. |
| final boolean attestationChallengeMatches = checkAttestationChallenge( |
| attestationAttributes, requirements.getByteArray(PARAM_CHALLENGE)); |
| if (!attestationChallengeMatches) { |
| debugVerboseLog( |
| "Provided challenge does not match leaf certificate challenge."); |
| return false; |
| } |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Unsupported local binding type " |
| + localBindingTypeToString(localBindingType)); |
| } |
| |
| // Second: check specified optional requirements. |
| if (requirements.containsKey(PARAM_OWNED_BY_SYSTEM)) { |
| if (requirements.getBoolean(PARAM_OWNED_BY_SYSTEM)) { |
| // Verify key is owned by the system. |
| final boolean ownedBySystem = checkOwnedBySystem( |
| leafCertificate, attestationAttributes); |
| if (!ownedBySystem) { |
| debugVerboseLog("Certificate public key is not owned by the AndroidSystem."); |
| return false; |
| } |
| } else { |
| throw new IllegalArgumentException("The value of the requirement key " |
| + PARAM_OWNED_BY_SYSTEM |
| + " cannot be false. You can remove the key if you don't want to verify " |
| + "it."); |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean checkAttestationForPeerDeviceProfile( |
| @NonNull AndroidKeystoreAttestationVerificationAttributes attestationAttributes) { |
| // Checks for support of Keymaster 4. |
| if (attestationAttributes.getAttestationVersion() < 3) { |
| debugVerboseLog("Attestation version is not at least 3 (Keymaster 4)."); |
| return false; |
| } |
| |
| // Checks for support of Keymaster 4. |
| if (attestationAttributes.getKeymasterVersion() < 4) { |
| debugVerboseLog("Keymaster version is not at least 4."); |
| return false; |
| } |
| |
| // First two characters are Android OS version. |
| if (attestationAttributes.getKeyOsVersion() < 100000) { |
| debugVerboseLog("Android OS version is not 10+."); |
| return false; |
| } |
| |
| if (!attestationAttributes.isAttestationHardwareBacked()) { |
| debugVerboseLog("Key is not HW backed."); |
| return false; |
| } |
| |
| if (!attestationAttributes.isKeymasterHardwareBacked()) { |
| debugVerboseLog("Keymaster is not HW backed."); |
| return false; |
| } |
| |
| if (attestationAttributes.getVerifiedBootState() != VERIFIED) { |
| debugVerboseLog("Boot state not Verified."); |
| return false; |
| } |
| |
| try { |
| if (!attestationAttributes.isVerifiedBootLocked()) { |
| debugVerboseLog("Verified boot state is not locked."); |
| return false; |
| } |
| } catch (IllegalStateException e) { |
| debugVerboseLog("VerifiedBootLocked is not set.", e); |
| return false; |
| } |
| |
| // Patch level integer YYYYMM is expected to be within 1 year of today. |
| if (!isValidPatchLevel(attestationAttributes.getKeyOsPatchLevel())) { |
| debugVerboseLog("OS patch level is not within valid range."); |
| return false; |
| } |
| |
| // Patch level integer YYYYMMDD is expected to be within 1 year of today. |
| if (!isValidPatchLevel(attestationAttributes.getKeyBootPatchLevel())) { |
| debugVerboseLog("Boot patch level is not within valid range."); |
| return false; |
| } |
| |
| if (!isValidPatchLevel(attestationAttributes.getKeyVendorPatchLevel())) { |
| debugVerboseLog("Vendor patch level is not within valid range."); |
| return false; |
| } |
| |
| if (!isValidPatchLevel(attestationAttributes.getKeyBootPatchLevel())) { |
| debugVerboseLog("Boot patch level is not within valid range."); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private boolean checkPublicKey( |
| @NonNull Certificate certificate, @NonNull byte[] expectedPublicKey) { |
| final byte[] publicKey = certificate.getPublicKey().getEncoded(); |
| return Arrays.equals(publicKey, expectedPublicKey); |
| } |
| |
| private boolean checkAttestationChallenge( |
| @NonNull AndroidKeystoreAttestationVerificationAttributes attestationAttributes, |
| @NonNull byte[] expectedChallenge) { |
| final byte[] challenge = attestationAttributes.getAttestationChallenge().toByteArray(); |
| return Arrays.equals(challenge, expectedChallenge); |
| } |
| |
| private boolean checkOwnedBySystem(@NonNull X509Certificate certificate, |
| @NonNull AndroidKeystoreAttestationVerificationAttributes attestationAttributes) { |
| final Set<String> ownerPackages = |
| attestationAttributes.getApplicationPackageNameVersion().keySet(); |
| if (!ANDROID_SYSTEM_PACKAGE_NAME_SET.equals(ownerPackages)) { |
| debugVerboseLog("Owner is not system, packages=" + ownerPackages); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Validates patchLevel passed is within range of the local device patch date if local patch is |
| * not over one year old. Since the time can be changed on device, just checking the patch date |
| * is not enough. Therefore, we also confirm the patch level for the remote and local device are |
| * similar. |
| */ |
| private boolean isValidPatchLevel(int patchLevel) { |
| LocalDate currentDate = mTestSystemDate != null |
| ? mTestSystemDate : LocalDate.now(ZoneId.systemDefault()); |
| |
| // Convert local patch date to LocalDate. |
| LocalDate localPatchDate; |
| try { |
| if (mTestLocalPatchDate != null) { |
| localPatchDate = mTestLocalPatchDate; |
| } else { |
| localPatchDate = LocalDate.parse(Build.VERSION.SECURITY_PATCH); |
| } |
| } catch (Throwable t) { |
| debugVerboseLog("Build.VERSION.SECURITY_PATCH: " |
| + Build.VERSION.SECURITY_PATCH + " is not in format YYYY-MM-DD"); |
| return false; |
| } |
| |
| // Check local patch date is not in last year of system clock. |
| if (ChronoUnit.MONTHS.between(localPatchDate, currentDate) > MAX_PATCH_AGE_MONTHS) { |
| return true; |
| } |
| |
| // Convert remote patch dates to LocalDate. |
| String remoteDeviceDateStr = String.valueOf(patchLevel); |
| if (remoteDeviceDateStr.length() != 6 && remoteDeviceDateStr.length() != 8) { |
| debugVerboseLog("Patch level is not in format YYYYMM or YYYYMMDD"); |
| return false; |
| } |
| |
| int patchYear = Integer.parseInt(remoteDeviceDateStr.substring(0, 4)); |
| int patchMonth = Integer.parseInt(remoteDeviceDateStr.substring(4, 6)); |
| LocalDate remotePatchDate = LocalDate.of(patchYear, patchMonth, 1); |
| |
| // Check patch dates are within 1 year of each other |
| boolean IsRemotePatchWithinOneYearOfLocalPatch; |
| if (remotePatchDate.compareTo(localPatchDate) > 0) { |
| IsRemotePatchWithinOneYearOfLocalPatch = ChronoUnit.MONTHS.between( |
| localPatchDate, remotePatchDate) <= MAX_PATCH_AGE_MONTHS; |
| } else if (remotePatchDate.compareTo(localPatchDate) < 0) { |
| IsRemotePatchWithinOneYearOfLocalPatch = ChronoUnit.MONTHS.between( |
| remotePatchDate, localPatchDate) <= MAX_PATCH_AGE_MONTHS; |
| } else { |
| IsRemotePatchWithinOneYearOfLocalPatch = true; |
| } |
| |
| return IsRemotePatchWithinOneYearOfLocalPatch; |
| } |
| |
| /** |
| * Checks certificate revocation status. |
| * |
| * Queries status list from android.googleapis.com/attestation/status and checks for |
| * the existence of certificate's serial number. If serial number exists in map, then fail. |
| */ |
| private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker { |
| private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; |
| private static final String STATUS_PROPERTY_KEY = "status"; |
| private static final String REASON_PROPERTY_KEY = "reason"; |
| private String mStatusUrl; |
| private JSONObject mJsonStatusMap; |
| |
| @Override |
| public void init(boolean forward) throws CertPathValidatorException { |
| mStatusUrl = getRevocationListUrl(); |
| if (mStatusUrl == null || mStatusUrl.isEmpty()) { |
| throw new CertPathValidatorException( |
| "R.string.vendor_required_attestation_revocation_list_url is empty."); |
| } |
| // TODO(b/221067843): Update to only pull status map on non critical path and if |
| // out of date (24hrs). |
| mJsonStatusMap = getStatusMap(mStatusUrl); |
| } |
| |
| @Override |
| public boolean isForwardCheckingSupported() { |
| return false; |
| } |
| |
| @Override |
| public Set<String> getSupportedExtensions() { |
| return null; |
| } |
| |
| @Override |
| public void check(Certificate cert, Collection<String> unresolvedCritExts) |
| throws CertPathValidatorException { |
| X509Certificate x509Certificate = (X509Certificate) cert; |
| // The json key is the certificate's serial number converted to lowercase hex. |
| String serialNumber = x509Certificate.getSerialNumber().toString(16); |
| |
| if (serialNumber == null) { |
| throw new CertPathValidatorException("Certificate serial number can not be null."); |
| } |
| |
| if (mJsonStatusMap.has(serialNumber)) { |
| JSONObject revocationStatus; |
| String status; |
| String reason; |
| try { |
| revocationStatus = mJsonStatusMap.getJSONObject(serialNumber); |
| status = revocationStatus.getString(STATUS_PROPERTY_KEY); |
| reason = revocationStatus.getString(REASON_PROPERTY_KEY); |
| } catch (Throwable t) { |
| throw new CertPathValidatorException("Unable get properties for certificate " |
| + "with serial number " + serialNumber); |
| } |
| throw new CertPathValidatorException( |
| "Invalid certificate with serial number " + serialNumber |
| + " has status " + status |
| + " because reason " + reason); |
| } |
| } |
| |
| private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException { |
| URL url; |
| try { |
| url = new URL(stringUrl); |
| } catch (Throwable t) { |
| throw new CertPathValidatorException( |
| "Unable to get revocation status from " + mStatusUrl, t); |
| } |
| |
| try (InputStream inputStream = url.openStream()) { |
| JSONObject statusListJson = new JSONObject( |
| new String(inputStream.readAllBytes(), UTF_8)); |
| return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); |
| } catch (Throwable t) { |
| throw new CertPathValidatorException( |
| "Unable to parse revocation status from " + mStatusUrl, t); |
| } |
| } |
| |
| private String getRevocationListUrl() { |
| return mContext.getResources().getString( |
| R.string.vendor_required_attestation_revocation_list_url); |
| } |
| } |
| |
| private static void debugVerboseLog(String str, Throwable t) { |
| if (DEBUG) { |
| Slog.v(TAG, str, t); |
| } |
| } |
| |
| private static void debugVerboseLog(String str) { |
| if (DEBUG) { |
| Slog.v(TAG, str); |
| } |
| } |
| } |