/*
 * 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 org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.DLTaggedObject;

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 DLTaggedObject)) {
                    continue;
                }

                DLTaggedObject taggedObject = (DLTaggedObject) 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 DLTaggedObject)) {
                        continue;
                    }

                    DLTaggedObject innerSequenceObj = (DLTaggedObject) 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);
    }
}
