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