• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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