1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.squareup.okhttp.internal.tls; 19 20import javax.security.auth.x500.X500Principal; 21 22/** 23 * A distinguished name (DN) parser. This parser only supports extracting a 24 * string value from a DN. It doesn't support values in the hex-string style. 25 */ 26final class DistinguishedNameParser { 27 private final String dn; 28 private final int length; 29 private int pos; 30 private int beg; 31 private int end; 32 33 /** Temporary variable to store positions of the currently parsed item. */ 34 private int cur; 35 36 /** Distinguished name characters. */ 37 private char[] chars; 38 39 public DistinguishedNameParser(X500Principal principal) { 40 // RFC2253 is used to ensure we get attributes in the reverse 41 // order of the underlying ASN.1 encoding, so that the most 42 // significant values of repeated attributes occur first. 43 this.dn = principal.getName(X500Principal.RFC2253); 44 this.length = this.dn.length(); 45 } 46 47 // gets next attribute type: (ALPHA 1*keychar) / oid 48 private String nextAT() { 49 // skip preceding space chars, they can present after 50 // comma or semicolon (compatibility with RFC 1779) 51 for (; pos < length && chars[pos] == ' '; pos++) { 52 } 53 if (pos == length) { 54 return null; // reached the end of DN 55 } 56 57 // mark the beginning of attribute type 58 beg = pos; 59 60 // attribute type chars 61 pos++; 62 for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) { 63 // we don't follow exact BNF syntax here: 64 // accept any char except space and '=' 65 } 66 if (pos >= length) { 67 throw new IllegalStateException("Unexpected end of DN: " + dn); 68 } 69 70 // mark the end of attribute type 71 end = pos; 72 73 // skip trailing space chars between attribute type and '=' 74 // (compatibility with RFC 1779) 75 if (chars[pos] == ' ') { 76 for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) { 77 } 78 79 if (chars[pos] != '=' || pos == length) { 80 throw new IllegalStateException("Unexpected end of DN: " + dn); 81 } 82 } 83 84 pos++; //skip '=' char 85 86 // skip space chars between '=' and attribute value 87 // (compatibility with RFC 1779) 88 for (; pos < length && chars[pos] == ' '; pos++) { 89 } 90 91 // in case of oid attribute type skip its prefix: "oid." or "OID." 92 // (compatibility with RFC 1779) 93 if ((end - beg > 4) && (chars[beg + 3] == '.') 94 && (chars[beg] == 'O' || chars[beg] == 'o') 95 && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') 96 && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { 97 beg += 4; 98 } 99 100 return new String(chars, beg, end - beg); 101 } 102 103 // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION 104 private String quotedAV() { 105 pos++; 106 beg = pos; 107 end = beg; 108 while (true) { 109 110 if (pos == length) { 111 throw new IllegalStateException("Unexpected end of DN: " + dn); 112 } 113 114 if (chars[pos] == '"') { 115 // enclosing quotation was found 116 pos++; 117 break; 118 } else if (chars[pos] == '\\') { 119 chars[end] = getEscaped(); 120 } else { 121 // shift char: required for string with escaped chars 122 chars[end] = chars[pos]; 123 } 124 pos++; 125 end++; 126 } 127 128 // skip trailing space chars before comma or semicolon. 129 // (compatibility with RFC 1779) 130 for (; pos < length && chars[pos] == ' '; pos++) { 131 } 132 133 return new String(chars, beg, end - beg); 134 } 135 136 // gets hex string attribute value: "#" hexstring 137 private String hexAV() { 138 if (pos + 4 >= length) { 139 // encoded byte array must be not less then 4 c 140 throw new IllegalStateException("Unexpected end of DN: " + dn); 141 } 142 143 beg = pos; // store '#' position 144 pos++; 145 while (true) { 146 147 // check for end of attribute value 148 // looks for space and component separators 149 if (pos == length || chars[pos] == '+' || chars[pos] == ',' 150 || chars[pos] == ';') { 151 end = pos; 152 break; 153 } 154 155 if (chars[pos] == ' ') { 156 end = pos; 157 pos++; 158 // skip trailing space chars before comma or semicolon. 159 // (compatibility with RFC 1779) 160 for (; pos < length && chars[pos] == ' '; pos++) { 161 } 162 break; 163 } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { 164 chars[pos] += 32; //to low case 165 } 166 167 pos++; 168 } 169 170 // verify length of hex string 171 // encoded byte array must be not less then 4 and must be even number 172 int hexLen = end - beg; // skip first '#' char 173 if (hexLen < 5 || (hexLen & 1) == 0) { 174 throw new IllegalStateException("Unexpected end of DN: " + dn); 175 } 176 177 // get byte encoding from string representation 178 byte[] encoded = new byte[hexLen / 2]; 179 for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { 180 encoded[i] = (byte) getByte(p); 181 } 182 183 return new String(chars, beg, hexLen); 184 } 185 186 // gets string attribute value: *( stringchar / pair ) 187 private String escapedAV() { 188 beg = pos; 189 end = pos; 190 while (true) { 191 if (pos >= length) { 192 // the end of DN has been found 193 return new String(chars, beg, end - beg); 194 } 195 196 switch (chars[pos]) { 197 case '+': 198 case ',': 199 case ';': 200 // separator char has been found 201 return new String(chars, beg, end - beg); 202 case '\\': 203 // escaped char 204 chars[end++] = getEscaped(); 205 pos++; 206 break; 207 case ' ': 208 // need to figure out whether space defines 209 // the end of attribute value or not 210 cur = end; 211 212 pos++; 213 chars[end++] = ' '; 214 215 for (; pos < length && chars[pos] == ' '; pos++) { 216 chars[end++] = ' '; 217 } 218 if (pos == length || chars[pos] == ',' || chars[pos] == '+' 219 || chars[pos] == ';') { 220 // separator char or the end of DN has been found 221 return new String(chars, beg, cur - beg); 222 } 223 break; 224 default: 225 chars[end++] = chars[pos]; 226 pos++; 227 } 228 } 229 } 230 231 // returns escaped char 232 private char getEscaped() { 233 pos++; 234 if (pos == length) { 235 throw new IllegalStateException("Unexpected end of DN: " + dn); 236 } 237 238 switch (chars[pos]) { 239 case '"': 240 case '\\': 241 case ',': 242 case '=': 243 case '+': 244 case '<': 245 case '>': 246 case '#': 247 case ';': 248 case ' ': 249 case '*': 250 case '%': 251 case '_': 252 //FIXME: escaping is allowed only for leading or trailing space char 253 return chars[pos]; 254 default: 255 // RFC doesn't explicitly say that escaped hex pair is 256 // interpreted as UTF-8 char. It only contains an example of such DN. 257 return getUTF8(); 258 } 259 } 260 261 // decodes UTF-8 char 262 // see http://www.unicode.org for UTF-8 bit distribution table 263 private char getUTF8() { 264 int res = getByte(pos); 265 pos++; //FIXME tmp 266 267 if (res < 128) { // one byte: 0-7F 268 return (char) res; 269 } else if (res >= 192 && res <= 247) { 270 271 int count; 272 if (res <= 223) { // two bytes: C0-DF 273 count = 1; 274 res = res & 0x1F; 275 } else if (res <= 239) { // three bytes: E0-EF 276 count = 2; 277 res = res & 0x0F; 278 } else { // four bytes: F0-F7 279 count = 3; 280 res = res & 0x07; 281 } 282 283 int b; 284 for (int i = 0; i < count; i++) { 285 pos++; 286 if (pos == length || chars[pos] != '\\') { 287 return 0x3F; //FIXME failed to decode UTF-8 char - return '?' 288 } 289 pos++; 290 291 b = getByte(pos); 292 pos++; //FIXME tmp 293 if ((b & 0xC0) != 0x80) { 294 return 0x3F; //FIXME failed to decode UTF-8 char - return '?' 295 } 296 297 res = (res << 6) + (b & 0x3F); 298 } 299 return (char) res; 300 } else { 301 return 0x3F; //FIXME failed to decode UTF-8 char - return '?' 302 } 303 } 304 305 // Returns byte representation of a char pair 306 // The char pair is composed of DN char in 307 // specified 'position' and the next char 308 // According to BNF syntax: 309 // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" 310 // / "a" / "b" / "c" / "d" / "e" / "f" 311 private int getByte(int position) { 312 if (position + 1 >= length) { 313 throw new IllegalStateException("Malformed DN: " + dn); 314 } 315 316 int b1, b2; 317 318 b1 = chars[position]; 319 if (b1 >= '0' && b1 <= '9') { 320 b1 = b1 - '0'; 321 } else if (b1 >= 'a' && b1 <= 'f') { 322 b1 = b1 - 87; // 87 = 'a' - 10 323 } else if (b1 >= 'A' && b1 <= 'F') { 324 b1 = b1 - 55; // 55 = 'A' - 10 325 } else { 326 throw new IllegalStateException("Malformed DN: " + dn); 327 } 328 329 b2 = chars[position + 1]; 330 if (b2 >= '0' && b2 <= '9') { 331 b2 = b2 - '0'; 332 } else if (b2 >= 'a' && b2 <= 'f') { 333 b2 = b2 - 87; // 87 = 'a' - 10 334 } else if (b2 >= 'A' && b2 <= 'F') { 335 b2 = b2 - 55; // 55 = 'A' - 10 336 } else { 337 throw new IllegalStateException("Malformed DN: " + dn); 338 } 339 340 return (b1 << 4) + b2; 341 } 342 343 /** 344 * Parses the DN and returns the most significant attribute value 345 * for an attribute type, or null if none found. 346 * 347 * @param attributeType attribute type to look for (e.g. "ca") 348 */ 349 public String findMostSpecific(String attributeType) { 350 // Initialize internal state. 351 pos = 0; 352 beg = 0; 353 end = 0; 354 cur = 0; 355 chars = dn.toCharArray(); 356 357 String attType = nextAT(); 358 if (attType == null) { 359 return null; 360 } 361 while (true) { 362 String attValue = ""; 363 364 if (pos == length) { 365 return null; 366 } 367 368 switch (chars[pos]) { 369 case '"': 370 attValue = quotedAV(); 371 break; 372 case '#': 373 attValue = hexAV(); 374 break; 375 case '+': 376 case ',': 377 case ';': // compatibility with RFC 1779: semicolon can separate RDNs 378 //empty attribute value 379 break; 380 default: 381 attValue = escapedAV(); 382 } 383 384 // Values are ordered from most specific to least specific 385 // due to the RFC2253 formatting. So take the first match 386 // we see. 387 if (attributeType.equalsIgnoreCase(attType)) { 388 return attValue; 389 } 390 391 if (pos >= length) { 392 return null; 393 } 394 395 if (chars[pos] == ',' || chars[pos] == ';') { 396 } else if (chars[pos] != '+') { 397 throw new IllegalStateException("Malformed DN: " + dn); 398 } 399 400 pos++; 401 attType = nextAT(); 402 if (attType == null) { 403 throw new IllegalStateException("Malformed DN: " + dn); 404 } 405 } 406 } 407} 408