1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wifi.hotspot2;
18 
19 import android.annotation.NonNull;
20 import android.text.TextUtils;
21 import android.util.Log;
22 import android.util.Pair;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import org.bouncycastle.asn1.ASN1Encodable;
27 import org.bouncycastle.asn1.ASN1InputStream;
28 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
29 import org.bouncycastle.asn1.ASN1Sequence;
30 import org.bouncycastle.asn1.DERUTF8String;
31 import org.bouncycastle.asn1.DLTaggedObject;
32 
33 import java.security.MessageDigest;
34 import java.security.NoSuchAlgorithmException;
35 import java.security.cert.X509Certificate;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.List;
40 import java.util.Locale;
41 
42 /**
43  * Utility class to validate a server X.509 Certificate of a service provider.
44  */
45 public class ServiceProviderVerifier {
46     private static final String TAG = "PasspointServiceProviderVerifier";
47 
48     private static final int OTHER_NAME = 0;
49     private static final int ENTRY_COUNT = 2;
50     private static final int LANGUAGE_CODE_LENGTH = 3;
51 
52     /**
53      * The Operator Friendly Name shall be an {@code otherName} sequence for the subjectAltName.
54      * If multiple Operator Friendly name values are required, then multiple {@code otherName}
55      * fields shall be present in the OSU certificate.
56      * The type-id of the {@code otherName} shall be an {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME}.
57      * {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} OBJECT IDENTIFIER ::= { 1.3.6.1.4.1.40808.1.1.1}
58      * The {@code ID_WFA_OID_HOTSPOT_FRIENDLYNAME} contains only one language code and
59      * friendly name for an operator and shall be encoded as an ASN.1 type UTF8String.
60      * Refer to 7.3.2 section in Hotspot 2.0 R2 Technical_Specification document in detail.
61      */
62     @VisibleForTesting
63     public static final String ID_WFA_OID_HOTSPOT_FRIENDLYNAME = "1.3.6.1.4.1.40808.1.1.1";
64 
65     /**
66      * Extracts provider names from a certificate by parsing subjectAltName extensions field
67      * as an otherName sequence, which contains
68      * id-wfa-hotspot-friendlyName oid + UTF8String denoting the friendlyName in the format below
69      * <languageCode><friendlyName>
70      * Note: Multiple language code will appear as additional UTF8 strings.
71      * Note: Multiple friendly names will appear as multiple otherName sequences.
72      *
73      * @param providerCert the X509Certificate to be parsed
74      * @return List of Pair representing {@Locale} and friendly Name for Operator found in the
75      * certificate.
76      */
getProviderNames(X509Certificate providerCert)77     public static List<Pair<Locale, String>> getProviderNames(X509Certificate providerCert) {
78         List<Pair<Locale, String>> providerNames = new ArrayList<>();
79         Pair<Locale, String> providerName;
80         if (providerCert == null) {
81             return providerNames;
82         }
83         try {
84             /**
85              *  The ASN.1 definition of the {@code SubjectAltName} extension is:
86              *  SubjectAltName ::= GeneralNames
87              *  GeneralNames :: = SEQUENCE SIZE (1..MAX) OF GeneralName
88              *
89              *  GeneralName ::= CHOICE {
90              *      otherName                       [0]     OtherName,
91              *      rfc822Name                      [1]     IA5String,
92              *      dNSName                         [2]     IA5String,
93              *      x400Address                     [3]     ORAddress,
94              *      directoryName                   [4]     Name,
95              *      ediPartyName                    [5]     EDIPartyName,
96              *      uniformResourceIdentifier       [6]     IA5String,
97              *      iPAddress                       [7]     OCTET STRING,
98              *      registeredID                    [8]     OBJECT IDENTIFIER}
99              *  If this certificate does not contain a SubjectAltName extension, null is returned.
100              *  Otherwise, a Collection is returned with an entry representing each
101              *  GeneralName included in the extension.
102              */
103             Collection<List<?>> col = providerCert.getSubjectAlternativeNames();
104             if (col == null) {
105                 return providerNames;
106             }
107             for (List<?> entry : col) {
108                 // Each entry is a List whose first entry is an Integer(the name type, 0-8)
109                 // and whose second entry is a String or a byte array.
110                 if (entry == null || entry.size() != ENTRY_COUNT) {
111                     continue;
112                 }
113 
114                 // The UTF-8 encoded Friendly Name shall be an otherName sequence.
115                 if ((Integer) entry.get(0) != OTHER_NAME) {
116                     continue;
117                 }
118 
119                 if (!(entry.toArray()[1] instanceof byte[])) {
120                     continue;
121                 }
122 
123                 byte[] octets = (byte[]) entry.toArray()[1];
124                 ASN1Encodable obj = new ASN1InputStream(octets).readObject();
125 
126                 if (!(obj instanceof DLTaggedObject)) {
127                     continue;
128                 }
129 
130                 DLTaggedObject taggedObject = (DLTaggedObject) obj;
131                 ASN1Encodable encodedObject = taggedObject.getObject();
132 
133                 if (!(encodedObject instanceof ASN1Sequence)) {
134                     continue;
135                 }
136 
137                 ASN1Sequence innerSequence = (ASN1Sequence) (encodedObject);
138                 ASN1Encodable innerObject = innerSequence.getObjectAt(0);
139 
140                 if (!(innerObject instanceof ASN1ObjectIdentifier)) {
141                     continue;
142                 }
143 
144                 ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(innerObject);
145                 if (!oid.getId().equals(ID_WFA_OID_HOTSPOT_FRIENDLYNAME)) {
146                     continue;
147                 }
148 
149                 for (int index = 1; index < innerSequence.size(); index++) {
150                     innerObject = innerSequence.getObjectAt(index);
151                     if (!(innerObject instanceof DLTaggedObject)) {
152                         continue;
153                     }
154 
155                     DLTaggedObject innerSequenceObj = (DLTaggedObject) innerObject;
156                     ASN1Encodable innerSequenceEncodedObject = innerSequenceObj.getObject();
157 
158                     if (!(innerSequenceEncodedObject instanceof DERUTF8String)) {
159                         continue;
160                     }
161 
162                     DERUTF8String providerNameUtf8 = (DERUTF8String) innerSequenceEncodedObject;
163                     providerName = getFriendlyName(providerNameUtf8.getString());
164                     if (providerName != null) {
165                         providerNames.add(providerName);
166                     }
167                 }
168             }
169         } catch (Exception e) {
170             e.printStackTrace();
171         }
172         return providerNames;
173     }
174 
175     /**
176      * Verifies a SHA-256 fingerprint of a X.509 Certificate.
177      *
178      * The SHA-256 fingerprint is calculated over the X.509 ASN.1 DER encoded certificate.
179      * @param x509Cert              a server X.509 Certificate to verify
180      * @param certSHA256Fingerprint a SHA-256 hash value stored in PPS(PerProviderSubscription)
181      *                              MO(Management Object)
182      *                              SubscriptionUpdate/TrustRoot/CertSHA256Fingerprint for
183      *                              remediation server
184      *                              AAAServerTrustRoot/CertSHA256Fingerprint for AAA server
185      *                              PolicyUpdate/TrustRoot/CertSHA256Fingerprint for Policy Server
186      *
187      * @return {@code true} if the fingerprint of {@code x509Cert} is equal to {@code
188      * certSHA256Fingerprint}, {@code false} otherwise.
189      */
verifyCertFingerprint(@onNull X509Certificate x509Cert, @NonNull byte[] certSHA256Fingerprint)190     public static boolean verifyCertFingerprint(@NonNull X509Certificate x509Cert,
191             @NonNull byte[] certSHA256Fingerprint) {
192         try {
193             byte[] fingerPrintSha256 = computeHash(x509Cert.getEncoded());
194             if (fingerPrintSha256 == null) return false;
195             if (Arrays.equals(fingerPrintSha256, certSHA256Fingerprint)) {
196                 return true;
197             }
198         } catch (Exception e) {
199             Log.e(TAG, "verifyCertFingerprint err:" + e);
200         }
201         return false;
202     }
203 
204     /**
205      * Computes a hash with SHA-256 algorithm for the input.
206      */
computeHash(byte[] input)207     private static byte[] computeHash(byte[] input) {
208         try {
209             MessageDigest digest = MessageDigest.getInstance("SHA-256");
210             return digest.digest(input);
211         } catch (NoSuchAlgorithmException e) {
212             return null;
213         }
214     }
215 
216     /**
217      * Extracts the language code and friendly Name from the alternativeName.
218      */
getFriendlyName(String alternativeName)219     private static Pair<Locale, String> getFriendlyName(String alternativeName) {
220 
221         // Check for the minimum required length.
222         if (TextUtils.isEmpty(alternativeName) || alternativeName.length() < LANGUAGE_CODE_LENGTH) {
223             return null;
224         }
225 
226         // Read the language string.
227         String language =  alternativeName.substring(0, LANGUAGE_CODE_LENGTH);
228         Locale locale;
229         try {
230             // The language code is a two or three character language code defined in ISO-639.
231             locale = new Locale.Builder().setLanguage(language).build();
232         } catch (Exception e) {
233             return null;
234         }
235 
236         // Read the friendlyName
237         String friendlyName = alternativeName.substring(LANGUAGE_CODE_LENGTH);
238         return Pair.create(locale, friendlyName);
239     }
240 }
241