1 package com.android.hotspot2.osu; 2 3 import android.util.Log; 4 5 import com.android.anqp.HSIconFileElement; 6 import com.android.anqp.I18Name; 7 import com.android.anqp.IconInfo; 8 import com.android.hotspot2.Utils; 9 import com.android.hotspot2.asn1.Asn1Class; 10 import com.android.hotspot2.asn1.Asn1Constructed; 11 import com.android.hotspot2.asn1.Asn1Decoder; 12 import com.android.hotspot2.asn1.Asn1Integer; 13 import com.android.hotspot2.asn1.Asn1Object; 14 import com.android.hotspot2.asn1.Asn1Octets; 15 import com.android.hotspot2.asn1.Asn1Oid; 16 import com.android.hotspot2.asn1.Asn1String; 17 import com.android.hotspot2.asn1.OidMappings; 18 import com.android.hotspot2.flow.OSUInfo; 19 20 import java.io.IOException; 21 import java.nio.ByteBuffer; 22 import java.nio.charset.StandardCharsets; 23 import java.security.GeneralSecurityException; 24 import java.security.MessageDigest; 25 import java.security.cert.X509Certificate; 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.HashMap; 29 import java.util.Iterator; 30 import java.util.List; 31 import java.util.Map; 32 33 public class SPVerifier { 34 public static final int OtherName = 0; 35 public static final int DNSName = 2; 36 37 private final OSUInfo mOSUInfo; 38 SPVerifier(OSUInfo osuInfo)39 public SPVerifier(OSUInfo osuInfo) { 40 mOSUInfo = osuInfo; 41 } 42 43 /* 44 SEQUENCE: 45 [Context 0]: 46 SEQUENCE: 47 [Context 0]: -- LogotypeData 48 SEQUENCE: 49 SEQUENCE: 50 SEQUENCE: 51 IA5String='image/png' 52 SEQUENCE: 53 SEQUENCE: 54 SEQUENCE: 55 OID=2.16.840.1.101.3.4.2.1 56 NULL 57 OCTET_STRING= cf aa 74 a8 ad af 85 82 06 c8 f5 b5 bf ee 45 72 8a ee ea bd 47 ab 50 d3 62 0c 92 c1 53 c3 4c 6b 58 SEQUENCE: 59 IA5String='http://www.r2-testbed.wi-fi.org/icon_orange_zxx.png' 60 SEQUENCE: 61 INTEGER=4184 62 INTEGER=-128 63 INTEGER=61 64 [Context 4]= 7a 78 78 65 [Context 0]: -- LogotypeData 66 SEQUENCE: 67 SEQUENCE: -- LogotypeImage 68 SEQUENCE: -- LogoTypeDetails 69 IA5String='image/png' 70 SEQUENCE: 71 SEQUENCE: -- HashAlgAndValue 72 SEQUENCE: 73 OID=2.16.840.1.101.3.4.2.1 74 NULL 75 OCTET_STRING= cb 35 5c ba 7a 21 59 df 8e 0a e1 d8 9f a4 81 9e 41 8f af 58 0c 08 d6 28 7f 66 22 98 13 57 95 8d 76 SEQUENCE: 77 IA5String='http://www.r2-testbed.wi-fi.org/icon_orange_eng.png' 78 SEQUENCE: -- LogotypeImageInfo 79 INTEGER=11635 80 INTEGER=-96 81 INTEGER=76 82 [Context 4]= 65 6e 67 83 */ 84 85 private static class LogoTypeImage { 86 private final String mMimeType; 87 private final List<HashAlgAndValue> mHashes = new ArrayList<>(); 88 private final List<String> mURIs = new ArrayList<>(); 89 private final int mFileSize; 90 private final int mXsize; 91 private final int mYsize; 92 private final String mLanguage; 93 LogoTypeImage(Asn1Constructed sequence)94 private LogoTypeImage(Asn1Constructed sequence) throws IOException { 95 Iterator<Asn1Object> children = sequence.getChildren().iterator(); 96 97 Iterator<Asn1Object> logoTypeDetails = 98 castObject(children.next(), Asn1Constructed.class).getChildren().iterator(); 99 mMimeType = castObject(logoTypeDetails.next(), Asn1String.class).getString(); 100 101 Asn1Constructed hashes = castObject(logoTypeDetails.next(), Asn1Constructed.class); 102 for (Asn1Object hash : hashes.getChildren()) { 103 mHashes.add(new HashAlgAndValue(castObject(hash, Asn1Constructed.class))); 104 } 105 Asn1Constructed urls = castObject(logoTypeDetails.next(), Asn1Constructed.class); 106 for (Asn1Object url : urls.getChildren()) { 107 mURIs.add(castObject(url, Asn1String.class).getString()); 108 } 109 110 boolean imageInfoSet = false; 111 int fileSize = -1; 112 int xSize = -1; 113 int ySize = -1; 114 String language = null; 115 116 if (children.hasNext()) { 117 Iterator<Asn1Object> imageInfo = 118 castObject(children.next(), Asn1Constructed.class).getChildren().iterator(); 119 120 Asn1Object first = imageInfo.next(); 121 if (first.getTag() == 0) { 122 first = imageInfo.next(); // Ignore optional LogotypeImageType 123 } 124 125 fileSize = (int) castObject(first, Asn1Integer.class).getValue(); 126 xSize = (int) castObject(imageInfo.next(), Asn1Integer.class).getValue(); 127 ySize = (int) castObject(imageInfo.next(), Asn1Integer.class).getValue(); 128 imageInfoSet = true; 129 130 if (imageInfo.hasNext()) { 131 Asn1Object next = imageInfo.next(); 132 if (next.getTag() != 4) { 133 next = imageInfo.hasNext() ? imageInfo.next() : null; // Skip resolution 134 } 135 if (next != null && next.getTag() == 4) { 136 language = new String(castObject(next, Asn1Octets.class).getOctets(), 137 StandardCharsets.US_ASCII); 138 } 139 } 140 } 141 142 if (imageInfoSet) { 143 mFileSize = complement(fileSize); 144 mXsize = complement(xSize); 145 mYsize = complement(ySize); 146 } else { 147 mFileSize = mXsize = mYsize = -1; 148 } 149 mLanguage = language; 150 } 151 verify(OSUInfo osuInfo)152 private boolean verify(OSUInfo osuInfo) throws GeneralSecurityException, IOException { 153 IconInfo iconInfo = osuInfo.getIconInfo(); 154 HSIconFileElement iconData = osuInfo.getIconFileElement(); 155 if (!iconInfo.getIconType().equals(mMimeType) || 156 !iconInfo.getLanguage().equals(mLanguage) || 157 iconData.getIconData().length != mFileSize) { 158 return false; 159 } 160 for (HashAlgAndValue hash : mHashes) { 161 if (hash.getJCEName() != null) { 162 MessageDigest digest = MessageDigest.getInstance(hash.getJCEName()); 163 byte[] computed = digest.digest(iconData.getIconData()); 164 if (!Arrays.equals(computed, hash.getHash())) { 165 throw new IOException("Icon hash mismatch"); 166 } else { 167 Log.d(OSUManager.TAG, "Icon verified with " + hash.getJCEName()); 168 return true; 169 } 170 } 171 } 172 return false; 173 } 174 175 @Override toString()176 public String toString() { 177 return "LogoTypeImage{" + 178 "MimeType='" + mMimeType + '\'' + 179 ", hashes=" + mHashes + 180 ", URIs=" + mURIs + 181 ", fileSize=" + mFileSize + 182 ", xSize=" + mXsize + 183 ", ySize=" + mYsize + 184 ", language='" + mLanguage + '\'' + 185 '}'; 186 } 187 } 188 189 private static class HashAlgAndValue { 190 private final String mJCEName; 191 private final byte[] mHash; 192 HashAlgAndValue(Asn1Constructed sequence)193 private HashAlgAndValue(Asn1Constructed sequence) throws IOException { 194 if (sequence.getChildren().size() != 2) { 195 throw new IOException("Bad HashAlgAndValue"); 196 } 197 Iterator<Asn1Object> children = sequence.getChildren().iterator(); 198 mJCEName = OidMappings.getJCEName(getFirstInner(children.next(), Asn1Oid.class)); 199 mHash = castObject(children.next(), Asn1Octets.class).getOctets(); 200 } 201 getJCEName()202 public String getJCEName() { 203 return mJCEName; 204 } 205 getHash()206 public byte[] getHash() { 207 return mHash; 208 } 209 210 @Override toString()211 public String toString() { 212 return "HashAlgAndValue{" + 213 "JCEName='" + mJCEName + '\'' + 214 ", hash=" + Utils.toHex(mHash) + 215 '}'; 216 } 217 } 218 complement(int value)219 private static int complement(int value) { 220 return value >= 0 ? value : (~value) + 1; 221 } 222 castObject(Asn1Object object, Class<T> klass)223 private static <T extends Asn1Object> T castObject(Asn1Object object, Class<T> klass) 224 throws IOException { 225 if (object.getClass() != klass) { 226 throw new IOException("Object is an " + object.getClass().getSimpleName() + 227 " expected an " + klass.getSimpleName()); 228 } 229 return klass.cast(object); 230 } 231 getFirstInner(Asn1Object container, Class<T> klass)232 private static <T extends Asn1Object> T getFirstInner(Asn1Object container, Class<T> klass) 233 throws IOException { 234 if (container.getClass() != Asn1Constructed.class) { 235 throw new IOException("Not a container"); 236 } 237 Iterator<Asn1Object> children = container.getChildren().iterator(); 238 if (!children.hasNext()) { 239 throw new IOException("No content"); 240 } 241 return castObject(children.next(), klass); 242 } 243 verify(X509Certificate osuCert)244 public void verify(X509Certificate osuCert) throws IOException, GeneralSecurityException { 245 if (osuCert == null) { 246 throw new IOException("No OSU cert found"); 247 } 248 249 checkName(castObject(getExtension(osuCert, OidMappings.IdCeSubjectAltName), 250 Asn1Constructed.class)); 251 252 List<LogoTypeImage> logos = getImageData(getExtension(osuCert, OidMappings.IdPeLogotype)); 253 Log.d(OSUManager.TAG, "Logos: " + logos); 254 for (LogoTypeImage logoTypeImage : logos) { 255 if (logoTypeImage.verify(mOSUInfo)) { 256 return; 257 } 258 } 259 throw new IOException("Failed to match icon against any cert logo"); 260 } 261 getImageData(Asn1Object logoExtension)262 private static List<LogoTypeImage> getImageData(Asn1Object logoExtension) throws IOException { 263 Asn1Constructed logo = castObject(logoExtension, Asn1Constructed.class); 264 Asn1Constructed communityLogo = castObject(logo.getChildren().iterator().next(), 265 Asn1Constructed.class); 266 if (communityLogo.getTag() != 0) { 267 throw new IOException("Expected tag [0] for communityLogos"); 268 } 269 270 List<LogoTypeImage> images = new ArrayList<>(); 271 Asn1Constructed communityLogoSeq = castObject(communityLogo.getChildren().iterator().next(), 272 Asn1Constructed.class); 273 for (Asn1Object logoTypeData : communityLogoSeq.getChildren()) { 274 if (logoTypeData.getTag() != 0) { 275 throw new IOException("Expected tag [0] for LogotypeData"); 276 } 277 for (Asn1Object logoTypeImage : castObject(logoTypeData.getChildren().iterator().next(), 278 Asn1Constructed.class).getChildren()) { 279 // only read the image SEQUENCE and skip any audio [1] tags 280 if (logoTypeImage.getAsn1Class() == Asn1Class.Universal) { 281 images.add(new LogoTypeImage(castObject(logoTypeImage, Asn1Constructed.class))); 282 } 283 } 284 } 285 return images; 286 } 287 checkName(Asn1Constructed altName)288 private void checkName(Asn1Constructed altName) throws IOException { 289 Map<String, I18Name> friendlyNames = new HashMap<>(); 290 for (Asn1Object name : altName.getChildren()) { 291 if (name.getAsn1Class() == Asn1Class.Context && name.getTag() == OtherName) { 292 Asn1Constructed otherName = (Asn1Constructed) name; 293 Iterator<Asn1Object> children = otherName.getChildren().iterator(); 294 if (children.hasNext()) { 295 Asn1Object oidObject = children.next(); 296 if (OidMappings.sIdWfaHotspotFriendlyName.equals(oidObject) && 297 children.hasNext()) { 298 Asn1Constructed value = castObject(children.next(), Asn1Constructed.class); 299 String text = castObject(value.getChildren().iterator().next(), 300 Asn1String.class).getString(); 301 I18Name friendlyName = new I18Name(text); 302 friendlyNames.put(friendlyName.getLanguage(), friendlyName); 303 } 304 } 305 } 306 } 307 Log.d(OSUManager.TAG, "Friendly names: " + friendlyNames.values()); 308 for (I18Name osuName : mOSUInfo.getOSUProvider().getNames()) { 309 I18Name friendlyName = friendlyNames.get(osuName.getLanguage()); 310 if (!osuName.equals(friendlyName)) { 311 throw new IOException("Friendly name '" + osuName + " not in certificate"); 312 } 313 } 314 } 315 getExtension(X509Certificate certificate, String extension)316 private static Asn1Object getExtension(X509Certificate certificate, String extension) 317 throws GeneralSecurityException, IOException { 318 byte[] data = certificate.getExtensionValue(extension); 319 if (data == null) { 320 return null; 321 } 322 Asn1Octets octetString = (Asn1Octets) Asn1Decoder.decode(ByteBuffer.wrap(data)). 323 iterator().next(); 324 Asn1Constructed sequence = castObject(Asn1Decoder.decode( 325 ByteBuffer.wrap(octetString.getOctets())).iterator().next(), 326 Asn1Constructed.class); 327 Log.d(OSUManager.TAG, "Extension " + extension + ": " + sequence); 328 return sequence; 329 } 330 } 331