1 /*
2  * Copyright (C) 2021 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.util;
18 
19 import android.annotation.NonNull;
20 import android.net.wifi.util.HexEncoding;
21 import android.text.TextUtils;
22 import android.util.Log;
23 
24 import java.nio.charset.StandardCharsets;
25 
26 /** Utilities for parsing information from a certificate subject. */
27 public class CertificateSubjectInfo {
28     private static final String TAG = "CertificateSubjectInfo";
29     private static final String COMMON_NAME_PREFIX = "CN=";
30     private static final String ORGANIZATION_PREFIX = "O=";
31     private static final String LOCATION_PREFIX = "L=";
32     private static final String STATE_PREFIX = "ST=";
33     private static final String COUNTRY_PREFIX = "C=";
34     // This is hex-encoded string.
35     private static final String EMAILADDRESS_OID_PREFIX = "1.2.840.113549.1.9.1=#1614";
36 
37     public String rawData = "";
38     public String commonName = "";
39     public String organization = "";
40     public String location = "";
41     public String state = "";
42     public String country = "";
43     public String email = "";
44 
CertificateSubjectInfo()45     private CertificateSubjectInfo() {
46     }
47 
48     /**
49      * Parse the subject of a certificate.
50      *
51      * @param subject the subject string
52      * @return CertificateSubjectInfo object if the subject is valid; otherwise, null.
53      */
parse(@onNull String subject)54     public static CertificateSubjectInfo parse(@NonNull String subject) {
55         if (subject == null) return null;
56         CertificateSubjectInfo info = new CertificateSubjectInfo();
57         info.rawData = subject;
58 
59         // Split the Subject line ignoring escaped commas
60         final String regex = "(?<!\\\\),";
61         String[] parts = info.rawData.split(regex);
62 
63         for (String s : parts) {
64             // Unescape escaped characters
65             s = unescapeString(s);
66             if (s == null) return null;
67             if (s.startsWith(COMMON_NAME_PREFIX) && TextUtils.isEmpty(info.commonName)) {
68                 info.commonName = s.substring(COMMON_NAME_PREFIX.length());
69             } else if (s.startsWith(ORGANIZATION_PREFIX) && TextUtils.isEmpty(info.organization)) {
70                 info.organization = s.substring(ORGANIZATION_PREFIX.length());
71             } else if (s.startsWith(LOCATION_PREFIX) && TextUtils.isEmpty(info.location)) {
72                 info.location = s.substring(LOCATION_PREFIX.length());
73             } else if (s.startsWith(STATE_PREFIX) && TextUtils.isEmpty(info.state)) {
74                 info.state = s.substring(STATE_PREFIX.length());
75             } else if (s.startsWith(COUNTRY_PREFIX) && TextUtils.isEmpty(info.country)) {
76                 info.country = s.substring(COUNTRY_PREFIX.length());
77             } else if (s.startsWith(EMAILADDRESS_OID_PREFIX) && TextUtils.isEmpty(info.email)) {
78                 String hexStr = s.substring(EMAILADDRESS_OID_PREFIX.length());
79                 try {
80                     info.email = new String(
81                             HexEncoding.decode(hexStr.toCharArray(), false),
82                             StandardCharsets.UTF_8);
83                 } catch (IllegalArgumentException ex) {
84                     Log.w(TAG, "Failed to decode email: " + ex);
85                 }
86             } else {
87                 Log.d(TAG, "Ignore an unknown or duplicate subject RDN: " + s);
88             }
89         }
90         if (TextUtils.isEmpty(info.commonName)) {
91             Log.e(TAG, "Parsed an invalid certificate without a common name");
92             return null;
93         }
94         return info;
95     }
96 
97     /**
98      * The characters in a subject string will be escaped based on RFC2253.
99      * To restore the original string, this method unescapes escaped
100      * characters.
101      */
unescapeString(String s)102     private static String unescapeString(String s) {
103         final String escapees = ",=+<>#;\"\\";
104         StringBuilder res = new StringBuilder();
105         char[] chars = s.toCharArray();
106         boolean isEscaped = false;
107         for (char c: chars) {
108             if (c == '\\' && !isEscaped) {
109                 isEscaped = true;
110                 continue;
111             }
112             // An illegal escaped character is founded.
113             if (isEscaped && escapees.indexOf(c) == -1) {
114                 Log.e(TAG, "Unable to unescape string: " + s);
115                 return null;
116             }
117             res.append(c);
118             isEscaped = false;
119         }
120         // There is a trailing '\' without a escaped character.
121         if (isEscaped) {
122             Log.e(TAG, "Unable to unescape string: " + s);
123             return null;
124         }
125         return res.toString();
126     }
127 
128     @Override
toString()129     public String toString() {
130         StringBuilder sb = new StringBuilder();
131         sb.append("Raw=").append(rawData)
132             .append(", Common Name=").append(commonName)
133             .append(", Organization=").append(organization)
134             .append(", Location=").append(location)
135             .append(", State=").append(state)
136             .append(", Country=").append(country)
137             .append(", Contact=").append(email);
138         return sb.toString();
139     }
140 }
141