content: /*
   * Copyright 2018 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.wifi.hotspot2;
  
  import android.annotation.NonNull;
  import android.text.TextUtils;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.org.bouncycastle.asn1.ASN1Encodable;
  import com.android.org.bouncycastle.asn1.ASN1InputStream;
  import com.android.org.bouncycastle.asn1.ASN1ObjectIdentifier;
  import com.android.org.bouncycastle.asn1.ASN1Sequence;
  import com.android.org.bouncycastle.asn1.DERTaggedObject;
  import com.android.org.bouncycastle.asn1.DERUTF8String;
  
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.security.cert.X509Certificate;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.List;
  import java.util.Locale;
  
  /**
   * Utility class to validate a server X.509 Certificate of a service provider.
   */
  public class ServiceProviderVerifier {
      private static final String TAG = "PasspointServiceProviderVerifier";
  
      private static final int OTHER_NAME = 0;
      private static final int ENTRY_COUNT = 2;
      private static final int LANGUAGE_CODE_LENGTH = 3;
  
      /**
       * The Operator Friendly Name shall be an {@code otherName} sequence for the subjectAltName.
       * If multiple Operator Friendly name values are required, then multiple {@code otherName}
       * fields shall be present in the OSU certificate.
       * The type-id of the {@code otherName} shall be an {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME}.
       * {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} OBJECT IDENTIFIER ::= { 1.3.6.1.4.1.40808.1.1.1}
       * The {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} contains only one language code and
       * friendly name for an operator and shall be encoded as an ASN.1 type UTF8String.
       * Refer to 7.3.2 section in Hotspot 2.0 R2 Technical_Specification document in detail.
       */
      @VisibleForTesting
      public static final String ID_WFA_OID_HOTSPOT_FRIENDLYNAME = "1.3.6.1.4.1.40808.1.1.1";
  
      /**
       * Extracts provider names from a certificate by parsing subjectAltName extensions field
       * as an otherName sequence, which contains
       * id-wfa-hotspot-friendlyName oid + UTF8String denoting the friendlyName in the format below
       * <languageCode><friendlyName>
       * Note: Multiple language code will appear as additional UTF8 strings.
       * Note: Multiple friendly names will appear as multiple otherName sequences.
       *
       * @param providerCert the X509Certificate to be parsed
       * @return List of Pair representing {@Locale} and friendly Name for Operator found in the
       * certificate.
       */
      public static List<Pair<Locale, String>> getProviderNames(X509Certificate providerCert) {
          List<Pair<Locale, String>> providerNames = new ArrayList<>();
          Pair<Locale, String> providerName;
          if (providerCert == null) {
              return providerNames;
          }
          try {
              /**
               *  The ASN.1 definition of the {@code SubjectAltName} extension is:
               *  SubjectAltName ::= GeneralNames
               *  GeneralNames :: = SEQUENCE SIZE (1..MAX) OF GeneralName
               *
               *  GeneralName ::= CHOICE {
               *      otherName                       [0]     OtherName,
               *      rfc822Name                      [1]     IA5String,
               *      dNSName                         [2]     IA5String,
               *      x400Address                     [3]     ORAddress,
               *      directoryName                   [4]     Name,
               *      ediPartyName                    [5]     EDIPartyName,
               *      uniformResourceIdentifier       [6]     IA5String,
               *      iPAddress                       [7]     OCTET STRING,
               *      registeredID                    [8]     OBJECT IDENTIFIER}
               *  If this certificate does not contain a SubjectAltName extension, null is returned.
               *  Otherwise, a Collection is returned with an entry representing each
               *  GeneralName included in the extension.
               */
              Collection<List<?>> col = providerCert.getSubjectAlternativeNames();
              if (col == null) {
                  return providerNames;
              }
              for (List<?> entry : col) {
                  // Each entry is a List whose first entry is an Integer(the name type, 0-8)
                  // and whose second entry is a String or a byte array.
                  if (entry == null || entry.size() != ENTRY_COUNT) {
                      continue;
                  }
  
                  // The UTF-8 encoded Friendly Name shall be an otherName sequence.
                  if ((Integer) entry.get(0) != OTHER_NAME) {
                      continue;
                  }
  
                  if (!(entry.toArray()[1] instanceof byte[])) {
                      continue;
                  }
  
                  byte[] octets = (byte[]) entry.toArray()[1];
                  ASN1Encodable obj = new ASN1InputStream(octets).readObject();
  
                  if (!(obj instanceof DERTaggedObject)) {
                      continue;
                  }
  
                  DERTaggedObject taggedObject = (DERTaggedObject) obj;
                  ASN1Encodable encodedObject = taggedObject.getObject();
  
                  if (!(encodedObject instanceof ASN1Sequence)) {
                      continue;
                  }
  
                  ASN1Sequence innerSequence = (ASN1Sequence) (encodedObject);
                  ASN1Encodable innerObject = innerSequence.getObjectAt(0);
  
                  if (!(innerObject instanceof ASN1ObjectIdentifier)) {
                      continue;
                  }
  
                  ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(innerObject);
                  if (!oid.getId().equals(ID_WFA_OID_HOTSPOT_FRIENDLYNAME)) {
                      continue;
                  }
  
                  for (int index = 1; index < innerSequence.size(); index++) {
                      innerObject = innerSequence.getObjectAt(index);
                      if (!(innerObject instanceof DERTaggedObject)) {
                          continue;
                      }
  
                      DERTaggedObject innerSequenceObj = (DERTaggedObject) innerObject;
                      ASN1Encodable innerSequenceEncodedObject = innerSequenceObj.getObject();
  
                      if (!(innerSequenceEncodedObject instanceof DERUTF8String)) {
                          continue;
                      }
  
                      DERUTF8String providerNameUtf8 = (DERUTF8String) innerSequenceEncodedObject;
                      providerName = getFriendlyName(providerNameUtf8.getString());
                      if (providerName != null) {
                          providerNames.add(providerName);
                      }
                  }
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
          return providerNames;
      }
  
      /**
       * Verifies a SHA-256 fingerprint of a X.509 Certificate.
       *
       * The SHA-256 fingerprint is calculated over the X.509 ASN.1 DER encoded certificate.
       * @param x509Cert              a server X.509 Certificate to verify
       * @param certSHA256Fingerprint a SHA-256 hash value stored in PPS(PerProviderSubscription)
       *                              MO(Management Object)
       *                              SubscriptionUpdate/TrustRoot/CertSHA256Fingerprint for
       *                              remediation server
       *                              AAAServerTrustRoot/CertSHA256Fingerprint for AAA server
       *                              PolicyUpdate/TrustRoot/CertSHA256Fingerprint for Policy Server
       *
       * @return {@code true} if the fingerprint of {@code x509Cert} is equal to {@code
       * certSHA256Fingerprint}, {@code false} otherwise.
       */
      public static boolean verifyCertFingerprint(@NonNull X509Certificate x509Cert,
              @NonNull byte[] certSHA256Fingerprint) {
          try {
              byte[] fingerPrintSha256 = computeHash(x509Cert.getEncoded());
              if (fingerPrintSha256 == null) return false;
              if (Arrays.equals(fingerPrintSha256, certSHA256Fingerprint)) {
                  return true;
              }
          } catch (Exception e) {
              Log.e(TAG, "verifyCertFingerprint err:" + e);
          }
          return false;
      }
  
      /**
       * Computes a hash with SHA-256 algorithm for the input.
       */
      private static byte[] computeHash(byte[] input) {
          try {
              MessageDigest digest = MessageDigest.getInstance("SHA-256");
              return digest.digest(input);
          } catch (NoSuchAlgorithmException e) {
              return null;
          }
      }
  
      /**
       * Extracts the language code and friendly Name from the alternativeName.
       */
      private static Pair<Locale, String> getFriendlyName(String alternativeName) {
  
          // Check for the minimum required length.
          if (TextUtils.isEmpty(alternativeName) || alternativeName.length() < LANGUAGE_CODE_LENGTH) {
              return null;
          }
  
          // Read the language string.
          String language =  alternativeName.substring(0, LANGUAGE_CODE_LENGTH);
          Locale locale;
          try {
              // The language code is a two or three character language code defined in ISO-639.
              locale = new Locale.Builder().setLanguage(language).build();
          } catch (Exception e) {
              return null;
          }
  
          // Read the friendlyName
          String friendlyName = alternativeName.substring(LANGUAGE_CODE_LENGTH);
          return Pair.create(locale, friendlyName);
      }
  }
  