1package com.android.hotspot2.osu;
2
3import android.util.Log;
4
5import com.android.anqp.HSIconFileElement;
6import com.android.anqp.I18Name;
7import com.android.anqp.IconInfo;
8import com.android.hotspot2.Utils;
9import com.android.hotspot2.asn1.Asn1Class;
10import com.android.hotspot2.asn1.Asn1Constructed;
11import com.android.hotspot2.asn1.Asn1Decoder;
12import com.android.hotspot2.asn1.Asn1Integer;
13import com.android.hotspot2.asn1.Asn1Object;
14import com.android.hotspot2.asn1.Asn1Octets;
15import com.android.hotspot2.asn1.Asn1Oid;
16import com.android.hotspot2.asn1.Asn1String;
17import com.android.hotspot2.asn1.OidMappings;
18import com.android.hotspot2.flow.OSUInfo;
19
20import java.io.IOException;
21import java.nio.ByteBuffer;
22import java.nio.charset.StandardCharsets;
23import java.security.GeneralSecurityException;
24import java.security.MessageDigest;
25import java.security.cert.X509Certificate;
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.HashMap;
29import java.util.Iterator;
30import java.util.List;
31import java.util.Map;
32
33public class SPVerifier {
34    public static final int OtherName = 0;
35    public static final int DNSName = 2;
36
37    private final OSUInfo mOSUInfo;
38
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
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
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
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
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
202        public String getJCEName() {
203            return mJCEName;
204        }
205
206        public byte[] getHash() {
207            return mHash;
208        }
209
210        @Override
211        public String toString() {
212            return "HashAlgAndValue{" +
213                    "JCEName='" + mJCEName + '\'' +
214                    ", hash=" + Utils.toHex(mHash) +
215                    '}';
216        }
217    }
218
219    private static int complement(int value) {
220        return value >= 0 ? value : (~value) + 1;
221    }
222
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
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
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
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
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
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