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