summaryrefslogtreecommitdiffstats
path: root/service/java/com/android/server/wifi/hotspot2/ServiceProviderVerifier.java
blob: d0f6dd541046f3be7d9616815cc3ce681a12b19f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wifi.hotspot2;

import android.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.android.internal.annotations.VisibleForTesting;
import com.android.org.bouncycastle.asn1.ASN1Encodable;
import com.android.org.bouncycastle.asn1.ASN1InputStream;
import com.android.org.bouncycastle.asn1.ASN1ObjectIdentifier;
import com.android.org.bouncycastle.asn1.ASN1Sequence;
import com.android.org.bouncycastle.asn1.DERTaggedObject;
import com.android.org.bouncycastle.asn1.DERUTF8String;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

/**
 * Utility class to validate a server X.509 Certificate of a service provider.
 */
public class ServiceProviderVerifier {
    private static final String TAG = "PasspointServiceProviderVerifier";

    private static final int OTHER_NAME = 0;
    private static final int ENTRY_COUNT = 2;
    private static final int LANGUAGE_CODE_LENGTH = 3;

    /**
     * The Operator Friendly Name shall be an {@code otherName} sequence for the subjectAltName.
     * If multiple Operator Friendly name values are required, then multiple {@code otherName}
     * fields shall be present in the OSU certificate.
     * The type-id of the {@code otherName} shall be an {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME}.
     * {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} OBJECT IDENTIFIER ::= { 1.3.6.1.4.1.40808.1.1.1}
     * The {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} contains only one language code and
     * friendly name for an operator and shall be encoded as an ASN.1 type UTF8String.
     * Refer to 7.3.2 section in Hotspot 2.0 R2 Technical_Specification document in detail.
     */
    @VisibleForTesting
    public static final String ID_WFA_OID_HOTSPOT_FRIENDLYNAME = "1.3.6.1.4.1.40808.1.1.1";

    /**
     * Extracts provider names from a certificate by parsing subjectAltName extensions field
     * as an otherName sequence, which contains
     * id-wfa-hotspot-friendlyName oid + UTF8String denoting the friendlyName in the format below
     * <languageCode><friendlyName>
     * Note: Multiple language code will appear as additional UTF8 strings.
     * Note: Multiple friendly names will appear as multiple otherName sequences.
     *
     * @param providerCert the X509Certificate to be parsed
     * @return List of Pair representing {@Locale} and friendly Name for Operator found in the
     * certificate.
     */
    public static List<Pair<Locale, String>> getProviderNames(X509Certificate providerCert) {
        List<Pair<Locale, String>> providerNames = new ArrayList<>();
        Pair<Locale, String> providerName;
        if (providerCert == null) {
            return providerNames;
        }
        try {
            /**
             *  The ASN.1 definition of the {@code SubjectAltName} extension is:
             *  SubjectAltName ::= GeneralNames
             *  GeneralNames :: = SEQUENCE SIZE (1..MAX) OF GeneralName
             *
             *  GeneralName ::= CHOICE {
             *      otherName                       [0]     OtherName,
             *      rfc822Name                      [1]     IA5String,
             *      dNSName                         [2]     IA5String,
             *      x400Address                     [3]     ORAddress,
             *      directoryName                   [4]     Name,
             *      ediPartyName                    [5]     EDIPartyName,
             *      uniformResourceIdentifier       [6]     IA5String,
             *      iPAddress                       [7]     OCTET STRING,
             *      registeredID                    [8]     OBJECT IDENTIFIER}
             *  If this certificate does not contain a SubjectAltName extension, null is returned.
             *  Otherwise, a Collection is returned with an entry representing each
             *  GeneralName included in the extension.
             */
            Collection<List<?>> col = providerCert.getSubjectAlternativeNames();
            if (col == null) {
                return providerNames;
            }
            for (List<?> entry : col) {
                // Each entry is a List whose first entry is an Integer(the name type, 0-8)
                // and whose second entry is a String or a byte array.
                if (entry == null || entry.size() != ENTRY_COUNT) {
                    continue;
                }

                // The UTF-8 encoded Friendly Name shall be an otherName sequence.
                if ((Integer) entry.get(0) != OTHER_NAME) {
                    continue;
                }

                if (!(entry.toArray()[1] instanceof byte[])) {
                    continue;
                }

                byte[] octets = (byte[]) entry.toArray()[1];
                ASN1Encodable obj = new ASN1InputStream(octets).readObject();

                if (!(obj instanceof DERTaggedObject)) {
                    continue;
                }

                DERTaggedObject taggedObject = (DERTaggedObject) obj;
                ASN1Encodable encodedObject = taggedObject.getObject();

                if (!(encodedObject instanceof ASN1Sequence)) {
                    continue;
                }

                ASN1Sequence innerSequence = (ASN1Sequence) (encodedObject);
                ASN1Encodable innerObject = innerSequence.getObjectAt(0);

                if (!(innerObject instanceof ASN1ObjectIdentifier)) {
                    continue;
                }

                ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(innerObject);
                if (!oid.getId().equals(ID_WFA_OID_HOTSPOT_FRIENDLYNAME)) {
                    continue;
                }

                for (int index = 1; index < innerSequence.size(); index++) {
                    innerObject = innerSequence.getObjectAt(index);
                    if (!(innerObject instanceof DERTaggedObject)) {
                        continue;
                    }

                    DERTaggedObject innerSequenceObj = (DERTaggedObject) innerObject;
                    ASN1Encodable innerSequenceEncodedObject = innerSequenceObj.getObject();

                    if (!(innerSequenceEncodedObject instanceof DERUTF8String)) {
                        continue;
                    }

                    DERUTF8String providerNameUtf8 = (DERUTF8String) innerSequenceEncodedObject;
                    providerName = getFriendlyName(providerNameUtf8.getString());
                    if (providerName != null) {
                        providerNames.add(providerName);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return providerNames;
    }

    /**
     * Verifies a SHA-256 fingerprint of a X.509 Certificate.
     *
     * The SHA-256 fingerprint is calculated over the X.509 ASN.1 DER encoded certificate.
     * @param x509Cert              a server X.509 Certificate to verify
     * @param certSHA256Fingerprint a SHA-256 hash value stored in PPS(PerProviderSubscription)
     *                              MO(Management Object)
     *                              SubscriptionUpdate/TrustRoot/CertSHA256Fingerprint for
     *                              remediation server
     *                              AAAServerTrustRoot/CertSHA256Fingerprint for AAA server
     *                              PolicyUpdate/TrustRoot/CertSHA256Fingerprint for Policy Server
     *
     * @return {@code true} if the fingerprint of {@code x509Cert} is equal to {@code
     * certSHA256Fingerprint}, {@code false} otherwise.
     */
    public static boolean verifyCertFingerprint(@NonNull X509Certificate x509Cert,
            @NonNull byte[] certSHA256Fingerprint) {
        try {
            byte[] fingerPrintSha256 = computeHash(x509Cert.getEncoded());
            if (fingerPrintSha256 == null) return false;
            if (Arrays.equals(fingerPrintSha256, certSHA256Fingerprint)) {
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "verifyCertFingerprint err:" + e);
        }
        return false;
    }

    /**
     * Computes a hash with SHA-256 algorithm for the input.
     */
    private static byte[] computeHash(byte[] input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return digest.digest(input);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * Extracts the language code and friendly Name from the alternativeName.
     */
    private static Pair<Locale, String> getFriendlyName(String alternativeName) {

        // Check for the minimum required length.
        if (TextUtils.isEmpty(alternativeName) || alternativeName.length() < LANGUAGE_CODE_LENGTH) {
            return null;
        }

        // Read the language string.
        String language =  alternativeName.substring(0, LANGUAGE_CODE_LENGTH);
        Locale locale;
        try {
            // The language code is a two or three character language code defined in ISO-639.
            locale = new Locale.Builder().setLanguage(language).build();
        } catch (Exception e) {
            return null;
        }

        // Read the friendlyName
        String friendlyName = alternativeName.substring(LANGUAGE_CODE_LENGTH);
        return Pair.create(locale, friendlyName);
    }
}