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