• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.List;
23 import 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  */
31 public 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 
DistinguishedNameParser(X500Principal principal)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
nextAT()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
quotedAV()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
hexAV()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 )
escapedAV()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
getEscaped()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
getUTF8()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"
getByte(int position)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      */
findMostSpecific(String attributeType)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      */
getAllMostSpecificFirst(String attributeType)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