1 /* 2 * Copyright 2018 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 com.android.server.wifi.hotspot2; 18 19 import android.annotation.NonNull; 20 import android.text.TextUtils; 21 import android.util.Log; 22 import android.util.Pair; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 26 import org.bouncycastle.asn1.ASN1Encodable; 27 import org.bouncycastle.asn1.ASN1InputStream; 28 import org.bouncycastle.asn1.ASN1ObjectIdentifier; 29 import org.bouncycastle.asn1.ASN1Sequence; 30 import org.bouncycastle.asn1.DERUTF8String; 31 import org.bouncycastle.asn1.DLTaggedObject; 32 33 import java.security.MessageDigest; 34 import java.security.NoSuchAlgorithmException; 35 import java.security.cert.X509Certificate; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Collection; 39 import java.util.List; 40 import java.util.Locale; 41 42 /** 43 * Utility class to validate a server X.509 Certificate of a service provider. 44 */ 45 public class ServiceProviderVerifier { 46 private static final String TAG = "PasspointServiceProviderVerifier"; 47 48 private static final int OTHER_NAME = 0; 49 private static final int ENTRY_COUNT = 2; 50 private static final int LANGUAGE_CODE_LENGTH = 3; 51 52 /** 53 * The Operator Friendly Name shall be an {@code otherName} sequence for the subjectAltName. 54 * If multiple Operator Friendly name values are required, then multiple {@code otherName} 55 * fields shall be present in the OSU certificate. 56 * The type-id of the {@code otherName} shall be an {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME}. 57 * {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} OBJECT IDENTIFIER ::= { 1.3.6.1.4.1.40808.1.1.1} 58 * The {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} contains only one language code and 59 * friendly name for an operator and shall be encoded as an ASN.1 type UTF8String. 60 * Refer to 7.3.2 section in Hotspot 2.0 R2 Technical_Specification document in detail. 61 */ 62 @VisibleForTesting 63 public static final String ID_WFA_OID_HOTSPOT_FRIENDLYNAME = "1.3.6.1.4.1.40808.1.1.1"; 64 65 /** 66 * Extracts provider names from a certificate by parsing subjectAltName extensions field 67 * as an otherName sequence, which contains 68 * id-wfa-hotspot-friendlyName oid + UTF8String denoting the friendlyName in the format below 69 * <languageCode><friendlyName> 70 * Note: Multiple language code will appear as additional UTF8 strings. 71 * Note: Multiple friendly names will appear as multiple otherName sequences. 72 * 73 * @param providerCert the X509Certificate to be parsed 74 * @return List of Pair representing {@Locale} and friendly Name for Operator found in the 75 * certificate. 76 */ getProviderNames(X509Certificate providerCert)77 public static List<Pair<Locale, String>> getProviderNames(X509Certificate providerCert) { 78 List<Pair<Locale, String>> providerNames = new ArrayList<>(); 79 Pair<Locale, String> providerName; 80 if (providerCert == null) { 81 return providerNames; 82 } 83 try { 84 /** 85 * The ASN.1 definition of the {@code SubjectAltName} extension is: 86 * SubjectAltName ::= GeneralNames 87 * GeneralNames :: = SEQUENCE SIZE (1..MAX) OF GeneralName 88 * 89 * GeneralName ::= CHOICE { 90 * otherName [0] OtherName, 91 * rfc822Name [1] IA5String, 92 * dNSName [2] IA5String, 93 * x400Address [3] ORAddress, 94 * directoryName [4] Name, 95 * ediPartyName [5] EDIPartyName, 96 * uniformResourceIdentifier [6] IA5String, 97 * iPAddress [7] OCTET STRING, 98 * registeredID [8] OBJECT IDENTIFIER} 99 * If this certificate does not contain a SubjectAltName extension, null is returned. 100 * Otherwise, a Collection is returned with an entry representing each 101 * GeneralName included in the extension. 102 */ 103 Collection<List<?>> col = providerCert.getSubjectAlternativeNames(); 104 if (col == null) { 105 return providerNames; 106 } 107 for (List<?> entry : col) { 108 // Each entry is a List whose first entry is an Integer(the name type, 0-8) 109 // and whose second entry is a String or a byte array. 110 if (entry == null || entry.size() != ENTRY_COUNT) { 111 continue; 112 } 113 114 // The UTF-8 encoded Friendly Name shall be an otherName sequence. 115 if ((Integer) entry.get(0) != OTHER_NAME) { 116 continue; 117 } 118 119 if (!(entry.toArray()[1] instanceof byte[])) { 120 continue; 121 } 122 123 byte[] octets = (byte[]) entry.toArray()[1]; 124 ASN1Encodable obj = new ASN1InputStream(octets).readObject(); 125 126 if (!(obj instanceof DLTaggedObject)) { 127 continue; 128 } 129 130 DLTaggedObject taggedObject = (DLTaggedObject) obj; 131 ASN1Encodable encodedObject = taggedObject.getObject(); 132 133 if (!(encodedObject instanceof ASN1Sequence)) { 134 continue; 135 } 136 137 ASN1Sequence innerSequence = (ASN1Sequence) (encodedObject); 138 ASN1Encodable innerObject = innerSequence.getObjectAt(0); 139 140 if (!(innerObject instanceof ASN1ObjectIdentifier)) { 141 continue; 142 } 143 144 ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(innerObject); 145 if (!oid.getId().equals(ID_WFA_OID_HOTSPOT_FRIENDLYNAME)) { 146 continue; 147 } 148 149 for (int index = 1; index < innerSequence.size(); index++) { 150 innerObject = innerSequence.getObjectAt(index); 151 if (!(innerObject instanceof DLTaggedObject)) { 152 continue; 153 } 154 155 DLTaggedObject innerSequenceObj = (DLTaggedObject) innerObject; 156 ASN1Encodable innerSequenceEncodedObject = innerSequenceObj.getObject(); 157 158 if (!(innerSequenceEncodedObject instanceof DERUTF8String)) { 159 continue; 160 } 161 162 DERUTF8String providerNameUtf8 = (DERUTF8String) innerSequenceEncodedObject; 163 providerName = getFriendlyName(providerNameUtf8.getString()); 164 if (providerName != null) { 165 providerNames.add(providerName); 166 } 167 } 168 } 169 } catch (Exception e) { 170 e.printStackTrace(); 171 } 172 return providerNames; 173 } 174 175 /** 176 * Verifies a SHA-256 fingerprint of a X.509 Certificate. 177 * 178 * The SHA-256 fingerprint is calculated over the X.509 ASN.1 DER encoded certificate. 179 * @param x509Cert a server X.509 Certificate to verify 180 * @param certSHA256Fingerprint a SHA-256 hash value stored in PPS(PerProviderSubscription) 181 * MO(Management Object) 182 * SubscriptionUpdate/TrustRoot/CertSHA256Fingerprint for 183 * remediation server 184 * AAAServerTrustRoot/CertSHA256Fingerprint for AAA server 185 * PolicyUpdate/TrustRoot/CertSHA256Fingerprint for Policy Server 186 * 187 * @return {@code true} if the fingerprint of {@code x509Cert} is equal to {@code 188 * certSHA256Fingerprint}, {@code false} otherwise. 189 */ verifyCertFingerprint(@onNull X509Certificate x509Cert, @NonNull byte[] certSHA256Fingerprint)190 public static boolean verifyCertFingerprint(@NonNull X509Certificate x509Cert, 191 @NonNull byte[] certSHA256Fingerprint) { 192 try { 193 byte[] fingerPrintSha256 = computeHash(x509Cert.getEncoded()); 194 if (fingerPrintSha256 == null) return false; 195 if (Arrays.equals(fingerPrintSha256, certSHA256Fingerprint)) { 196 return true; 197 } 198 } catch (Exception e) { 199 Log.e(TAG, "verifyCertFingerprint err:" + e); 200 } 201 return false; 202 } 203 204 /** 205 * Computes a hash with SHA-256 algorithm for the input. 206 */ computeHash(byte[] input)207 private static byte[] computeHash(byte[] input) { 208 try { 209 MessageDigest digest = MessageDigest.getInstance("SHA-256"); 210 return digest.digest(input); 211 } catch (NoSuchAlgorithmException e) { 212 return null; 213 } 214 } 215 216 /** 217 * Extracts the language code and friendly Name from the alternativeName. 218 */ getFriendlyName(String alternativeName)219 private static Pair<Locale, String> getFriendlyName(String alternativeName) { 220 221 // Check for the minimum required length. 222 if (TextUtils.isEmpty(alternativeName) || alternativeName.length() < LANGUAGE_CODE_LENGTH) { 223 return null; 224 } 225 226 // Read the language string. 227 String language = alternativeName.substring(0, LANGUAGE_CODE_LENGTH); 228 Locale locale; 229 try { 230 // The language code is a two or three character language code defined in ISO-639. 231 locale = new Locale.Builder().setLanguage(language).build(); 232 } catch (Exception e) { 233 return null; 234 } 235 236 // Read the friendlyName 237 String friendlyName = alternativeName.substring(LANGUAGE_CODE_LENGTH); 238 return Pair.create(locale, friendlyName); 239 } 240 } 241