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