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