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