/* * 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 * * 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> getProviderNames(X509Certificate providerCert) { List> providerNames = new ArrayList<>(); Pair 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> 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 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); } }