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 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 */
28public 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