• 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 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