Home | History | Annotate | Download | only in osu
      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 
     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 
     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 
    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
    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 
    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 
    201         public String getJCEName() {
    202             return mJCEName;
    203         }
    204 
    205         public byte[] getHash() {
    206             return mHash;
    207         }
    208 
    209         @Override
    210         public String toString() {
    211             return "HashAlgAndValue{" +
    212                     "JCEName='" + mJCEName + '\'' +
    213                     ", hash=" + Utils.toHex(mHash) +
    214                     '}';
    215         }
    216     }
    217 
    218     private static int complement(int value) {
    219         return value >= 0 ? value : (~value) + 1;
    220     }
    221 
    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 
    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 
    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 
    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 
    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 
    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