Home | History | Annotate | Download | only in ssl
      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