Home | History | Annotate | Download | only in x509
      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 /**
     19 * @author Alexander V. Esin, Stepan M. Mishura
     20 * @version $Revision$
     21 */
     22 
     23 package org.apache.harmony.security.x509;
     24 
     25 import java.io.IOException;
     26 import java.util.ArrayList;
     27 import java.util.List;
     28 import org.apache.harmony.security.utils.ObjectIdentifier;
     29 import org.apache.harmony.security.x501.AttributeTypeAndValue;
     30 import org.apache.harmony.security.x501.AttributeValue;
     31 
     32 /**
     33  * Distinguished Name Parser.
     34  *
     35  * Parses a distinguished name(DN) string according
     36  * BNF syntax specified in RFC 2253 and RFC 1779
     37  *
     38  * RFC 2253: Lightweight Directory Access Protocol (v3):
     39  *           UTF-8 String Representation of Distinguished Names
     40  *   http://www.ietf.org/rfc/rfc2253.txt
     41  *
     42  * RFC 1779: A String Representation of Distinguished Names
     43  *   http://www.ietf.org/rfc/rfc1779.txt
     44  */
     45 public final class DNParser {
     46     private int pos;
     47     private int beg;
     48     private int end;
     49 
     50     /** distinguished name chars */
     51     private final char[] chars;
     52 
     53     /** raw string contains '"' or '\' */
     54     private boolean hasQE;
     55 
     56     /** DER encoding of currently parsed item */
     57     private byte[] encoded;
     58 
     59     /**
     60      * @param dn - distinguished name string to be parsed
     61      */
     62     public DNParser(String dn) throws IOException {
     63         chars = dn.toCharArray();
     64     }
     65 
     66     /**
     67      * Returns the next attribute type: (ALPHA 1*keychar) / oid
     68      */
     69     private String nextAT() throws IOException {
     70         hasQE = false; // reset
     71 
     72         // skip preceding space chars, they can present after
     73         // comma or semicolon (compatibility with RFC 1779)
     74         for (; pos < chars.length && chars[pos] == ' '; pos++) {
     75         }
     76         if (pos == chars.length) {
     77             return null; // reached the end of DN
     78         }
     79 
     80         // mark the beginning of attribute type
     81         beg = pos;
     82 
     83         // attribute type chars
     84         pos++;
     85         for (; pos < chars.length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
     86             // we don't follow exact BNF syntax here:
     87             // accept any char except space and '='
     88         }
     89         if (pos >= chars.length) {
     90             // unexpected end of DN
     91             throw new IOException("Invalid distinguished name string");
     92         }
     93 
     94         // mark the end of attribute type
     95         end = pos;
     96 
     97         // skip trailing space chars between attribute type and '='
     98         // (compatibility with RFC 1779)
     99         if (chars[pos] == ' ') {
    100             for (; pos < chars.length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
    101             }
    102 
    103             if (chars[pos] != '=' || pos == chars.length) {
    104                 // unexpected end of DN
    105                 throw new IOException("Invalid distinguished name string");
    106             }
    107         }
    108 
    109         pos++; //skip '=' char
    110 
    111         // skip space chars between '=' and attribute value
    112         // (compatibility with RFC 1779)
    113         for (; pos < chars.length && chars[pos] == ' '; pos++) {
    114         }
    115 
    116         // in case of oid attribute type skip its prefix: "oid." or "OID."
    117         // (compatibility with RFC 1779)
    118         if ((end - beg > 4) && (chars[beg + 3] == '.')
    119                 && (chars[beg] == 'O' || chars[beg] == 'o')
    120                 && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
    121                 && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
    122             beg += 4;
    123         }
    124 
    125         return new String(chars, beg, end - beg);
    126     }
    127 
    128     /**
    129      * Returns a quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
    130      */
    131     private String quotedAV() throws IOException {
    132         pos++;
    133         beg = pos;
    134         end = beg;
    135         while (true) {
    136             if (pos == chars.length) {
    137                 // unexpected end of DN
    138                 throw new IOException("Invalid distinguished name string");
    139             }
    140 
    141             if (chars[pos] == '"') {
    142                 // enclosing quotation was found
    143                 pos++;
    144                 break;
    145             } else if (chars[pos] == '\\') {
    146                 chars[end] = getEscaped();
    147             } else {
    148                 // shift char: required for string with escaped chars
    149                 chars[end] = chars[pos];
    150             }
    151             pos++;
    152             end++;
    153         }
    154 
    155         // skip trailing space chars before comma or semicolon.
    156         // (compatibility with RFC 1779)
    157         for (; pos < chars.length && chars[pos] == ' '; pos++) {
    158         }
    159 
    160         return new String(chars, beg, end - beg);
    161     }
    162 
    163     /**
    164      * Returns a hex string attribute value: "#" hexstring
    165      */
    166     private String hexAV() throws IOException {
    167         if (pos + 4 >= chars.length) {
    168             // encoded byte array  must be not less then 4 c
    169             throw new IOException("Invalid distinguished name string");
    170         }
    171 
    172         beg = pos; // store '#' position
    173         pos++;
    174         while (true) {
    175             // check for end of attribute value
    176             // looks for space and component separators
    177             if (pos == chars.length || chars[pos] == '+' || chars[pos] == ','
    178                     || chars[pos] == ';') {
    179                 end = pos;
    180                 break;
    181             }
    182 
    183             if (chars[pos] == ' ') {
    184                 end = pos;
    185                 pos++;
    186                 // skip trailing space chars before comma or semicolon.
    187                 // (compatibility with RFC 1779)
    188                 for (; pos < chars.length && chars[pos] == ' '; pos++) {
    189                 }
    190                 break;
    191             } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
    192                 chars[pos] += 32; //to low case
    193             }
    194 
    195             pos++;
    196         }
    197 
    198         // verify length of hex string
    199         // encoded byte array  must be not less then 4 and must be even number
    200         int hexLen = end - beg; // skip first '#' char
    201         if (hexLen < 5 || (hexLen & 1) == 0) {
    202             throw new IOException("Invalid distinguished name string");
    203         }
    204 
    205         // get byte encoding from string representation
    206         encoded = new byte[hexLen / 2];
    207         for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
    208             encoded[i] = (byte) getByte(p);
    209         }
    210 
    211         return new String(chars, beg, hexLen);
    212     }
    213 
    214     /**
    215      * Returns a string attribute value: *( stringchar / pair ).
    216      */
    217     private String escapedAV() throws IOException {
    218         beg = pos;
    219         end = pos;
    220         while (true) {
    221             if (pos >= chars.length) {
    222                 // the end of DN has been found
    223                 return new String(chars, beg, end - beg);
    224             }
    225 
    226             switch (chars[pos]) {
    227             case '+':
    228             case ',':
    229             case ';':
    230                 // separator char has been found
    231                 return new String(chars, beg, end - beg);
    232             case '\\':
    233                 // escaped char
    234                 chars[end++] = getEscaped();
    235                 pos++;
    236                 break;
    237             case ' ':
    238                 // need to figure out whether space defines
    239                 // the end of attribute value or not
    240                 int cur = end;
    241 
    242                 pos++;
    243                 chars[end++] = ' ';
    244 
    245                 for (; pos < chars.length && chars[pos] == ' '; pos++) {
    246                     chars[end++] = ' ';
    247                 }
    248                 if (pos == chars.length || chars[pos] == ',' || chars[pos] == '+'
    249                         || chars[pos] == ';') {
    250                     // separator char or the end of DN has been found
    251                     return new String(chars, beg, cur - beg);
    252                 }
    253                 break;
    254             default:
    255                 chars[end++] = chars[pos];
    256                 pos++;
    257             }
    258         }
    259     }
    260 
    261     /**
    262      * Returns an escaped char
    263      */
    264     private char getEscaped() throws IOException {
    265         pos++;
    266         if (pos == chars.length) {
    267             throw new IOException("Invalid distinguished name string");
    268         }
    269 
    270         char ch = chars[pos];
    271         switch (ch) {
    272         case '"':
    273         case '\\':
    274             hasQE = true;
    275             return ch;
    276         case ',':
    277         case '=':
    278         case '+':
    279         case '<':
    280         case '>':
    281         case '#':
    282         case ';':
    283         case ' ':
    284         case '*':
    285         case '%':
    286         case '_':
    287             //FIXME: escaping is allowed only for leading or trailing space char
    288             return ch;
    289         default:
    290             // RFC doesn't explicitly say that escaped hex pair is
    291             // interpreted as UTF-8 char. It only contains an example of such DN.
    292             return getUTF8();
    293         }
    294     }
    295 
    296     /**
    297      * Decodes a UTF-8 char.
    298      */
    299     protected char getUTF8() throws IOException {
    300         int res = getByte(pos);
    301         pos++; //FIXME tmp
    302 
    303         if (res < 128) { // one byte: 0-7F
    304             return (char) res;
    305         } else if (res >= 192 && res <= 247) {
    306 
    307             int count;
    308             if (res <= 223) { // two bytes: C0-DF
    309                 count = 1;
    310                 res = res & 0x1F;
    311             } else if (res <= 239) { // three bytes: E0-EF
    312                 count = 2;
    313                 res = res & 0x0F;
    314             } else { // four bytes: F0-F7
    315                 count = 3;
    316                 res = res & 0x07;
    317             }
    318 
    319             int b;
    320             for (int i = 0; i < count; i++) {
    321                 pos++;
    322                 if (pos == chars.length || chars[pos] != '\\') {
    323                     return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
    324                 }
    325                 pos++;
    326 
    327                 b = getByte(pos);
    328                 pos++; //FIXME tmp
    329                 if ((b & 0xC0) != 0x80) {
    330                     return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
    331                 }
    332 
    333                 res = (res << 6) + (b & 0x3F);
    334             }
    335             return (char) res;
    336         } else {
    337             return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
    338         }
    339     }
    340 
    341     /**
    342      * Returns byte representation of a char pair
    343      * The char pair is composed of DN char in
    344      * specified 'position' and the next char
    345      * According to BNF syntax:
    346      * hexchar    = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
    347      *                     / "a" / "b" / "c" / "d" / "e" / "f"
    348      */
    349     private int getByte(int position) throws IOException {
    350         if ((position + 1) >= chars.length) {
    351             // to avoid ArrayIndexOutOfBoundsException
    352             throw new IOException("Invalid distinguished name string");
    353         }
    354 
    355         int b1 = chars[position];
    356         if (b1 >= '0' && b1 <= '9') {
    357             b1 = b1 - '0';
    358         } else if (b1 >= 'a' && b1 <= 'f') {
    359             b1 = b1 - 87; // 87 = 'a' - 10
    360         } else if (b1 >= 'A' && b1 <= 'F') {
    361             b1 = b1 - 55; // 55 = 'A' - 10
    362         } else {
    363             throw new IOException("Invalid distinguished name string");
    364         }
    365 
    366         int b2 = chars[position + 1];
    367         if (b2 >= '0' && b2 <= '9') {
    368             b2 = b2 - '0';
    369         } else if (b2 >= 'a' && b2 <= 'f') {
    370             b2 = b2 - 87; // 87 = 'a' - 10
    371         } else if (b2 >= 'A' && b2 <= 'F') {
    372             b2 = b2 - 55; // 55 = 'A' - 10
    373         } else {
    374             throw new IOException("Invalid distinguished name string");
    375         }
    376 
    377         return (b1 << 4) + b2;
    378     }
    379 
    380     /**
    381      * Parses DN
    382      *
    383      * @return a list of Relative Distinguished Names(RDN),
    384      *         each RDN is represented as a list of AttributeTypeAndValue objects
    385      */
    386     public List<List<AttributeTypeAndValue>> parse() throws IOException {
    387         List<List<AttributeTypeAndValue>> list = new ArrayList<List<AttributeTypeAndValue>>();
    388 
    389         String attType = nextAT();
    390         if (attType == null) {
    391             return list; //empty list of RDNs
    392         }
    393         ObjectIdentifier oid = AttributeTypeAndValue.getObjectIdentifier(attType);
    394 
    395         List<AttributeTypeAndValue> atav = new ArrayList<AttributeTypeAndValue>();
    396         while (true) {
    397             if (pos == chars.length) {
    398                 //empty Attribute Value
    399                 atav.add(new AttributeTypeAndValue(oid, new AttributeValue("", false, oid)));
    400                 list.add(0, atav);
    401                 return list;
    402             }
    403 
    404             switch (chars[pos]) {
    405             case '"':
    406                 atav.add(new AttributeTypeAndValue(oid, new AttributeValue(quotedAV(), hasQE, oid)));
    407                 break;
    408             case '#':
    409                 atav.add(new AttributeTypeAndValue(oid, new AttributeValue(hexAV(), encoded)));
    410                 break;
    411             case '+':
    412             case ',':
    413             case ';': // compatibility with RFC 1779: semicolon can separate RDNs
    414                 //empty attribute value
    415                 atav.add(new AttributeTypeAndValue(oid, new AttributeValue("", false, oid)));
    416                 break;
    417             default:
    418                 atav.add(new AttributeTypeAndValue(oid,
    419                                                    new AttributeValue(escapedAV(), hasQE, oid)));
    420             }
    421 
    422             if (pos >= chars.length) {
    423                 list.add(0, atav);
    424                 return list;
    425             }
    426 
    427             if (chars[pos] == ',' || chars[pos] == ';') {
    428                 list.add(0, atav);
    429                 atav = new ArrayList<AttributeTypeAndValue>();
    430             } else if (chars[pos] != '+') {
    431                 throw new IOException("Invalid distinguished name string");
    432             }
    433 
    434             pos++;
    435             attType = nextAT();
    436             if (attType == null) {
    437                 throw new IOException("Invalid distinguished name string");
    438             }
    439             oid = AttributeTypeAndValue.getObjectIdentifier(attType);
    440         }
    441     }
    442 }
    443