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;
18
19import java.io.IOException;
20import java.nio.ByteBuffer;
21import java.nio.charset.StandardCharsets;
22import java.security.GeneralSecurityException;
23import java.security.MessageDigest;
24import java.security.cert.X509Certificate;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.HashMap;
28import java.util.Iterator;
29import java.util.List;
30import java.util.Map;
31
32public 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