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