/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @author Alexander V. Esin, Stepan M. Mishura * @version $Revision$ */ package org.apache.harmony.security.x509; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.harmony.security.x501.AttributeTypeAndValue; import org.apache.harmony.security.x501.AttributeValue; /** * Distinguished Name Parser. * * Parses a distinguished name(DN) string according * BNF syntax specified in RFC 2253 and RFC 1779 * * RFC 2253: Lightweight Directory Access Protocol (v3): * UTF-8 String Representation of Distinguished Names * http://www.ietf.org/rfc/rfc2253.txt * * RFC 1779: A String Representation of Distinguished Names * http://www.ietf.org/rfc/rfc1779.txt */ public final class DNParser { private int pos; private int beg; private int end; /** distinguished name chars */ private final char[] chars; /** raw string contains '"' or '\' */ private boolean hasQE; /** DER encoding of currently parsed item */ private byte[] encoded; /** * @param dn - distinguished name string to be parsed */ public DNParser(String dn) throws IOException { chars = dn.toCharArray(); } /** * Returns the next attribute type: (ALPHA 1*keychar) / oid */ private String nextAT() throws IOException { hasQE = false; // reset // skip preceding space chars, they can present after // comma or semicolon (compatibility with RFC 1779) for (; pos < chars.length && chars[pos] == ' '; pos++) { } if (pos == chars.length) { return null; // reached the end of DN } // mark the beginning of attribute type beg = pos; // attribute type chars pos++; for (; pos < chars.length && chars[pos] != '=' && chars[pos] != ' '; pos++) { // we don't follow exact BNF syntax here: // accept any char except space and '=' } if (pos >= chars.length) { // unexpected end of DN throw new IOException("Invalid distinguished name string"); } // mark the end of attribute type end = pos; // skip trailing space chars between attribute type and '=' // (compatibility with RFC 1779) if (chars[pos] == ' ') { for (; pos < chars.length && chars[pos] != '=' && chars[pos] == ' '; pos++) { } if (chars[pos] != '=' || pos == chars.length) { // unexpected end of DN throw new IOException("Invalid distinguished name string"); } } pos++; //skip '=' char // skip space chars between '=' and attribute value // (compatibility with RFC 1779) for (; pos < chars.length && chars[pos] == ' '; pos++) { } // in case of oid attribute type skip its prefix: "oid." or "OID." // (compatibility with RFC 1779) if ((end - beg > 4) && (chars[beg + 3] == '.') && (chars[beg] == 'O' || chars[beg] == 'o') && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { beg += 4; } return new String(chars, beg, end - beg); } /** * Returns a quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION */ private String quotedAV() throws IOException { pos++; beg = pos; end = beg; while (true) { if (pos == chars.length) { // unexpected end of DN throw new IOException("Invalid distinguished name string"); } if (chars[pos] == '"') { // enclosing quotation was found pos++; break; } else if (chars[pos] == '\\') { chars[end] = getEscaped(); } else { // shift char: required for string with escaped chars chars[end] = chars[pos]; } pos++; end++; } // skip trailing space chars before comma or semicolon. // (compatibility with RFC 1779) for (; pos < chars.length && chars[pos] == ' '; pos++) { } return new String(chars, beg, end - beg); } /** * Returns a hex string attribute value: "#" hexstring */ private String hexAV() throws IOException { if (pos + 4 >= chars.length) { // encoded byte array must be not less then 4 c throw new IOException("Invalid distinguished name string"); } beg = pos; // store '#' position pos++; while (true) { // check for end of attribute value // looks for space and component separators if (pos == chars.length || chars[pos] == '+' || chars[pos] == ',' || chars[pos] == ';') { end = pos; break; } if (chars[pos] == ' ') { end = pos; pos++; // skip trailing space chars before comma or semicolon. // (compatibility with RFC 1779) for (; pos < chars.length && chars[pos] == ' '; pos++) { } break; } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { chars[pos] += 32; //to low case } pos++; } // verify length of hex string // encoded byte array must be not less then 4 and must be even number int hexLen = end - beg; // skip first '#' char if (hexLen < 5 || (hexLen & 1) == 0) { throw new IOException("Invalid distinguished name string"); } // get byte encoding from string representation encoded = new byte[hexLen / 2]; for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { encoded[i] = (byte) getByte(p); } return new String(chars, beg, hexLen); } /** * Returns a string attribute value: *( stringchar / pair ). */ private String escapedAV() throws IOException { beg = pos; end = pos; while (true) { if (pos >= chars.length) { // the end of DN has been found return new String(chars, beg, end - beg); } switch (chars[pos]) { case '+': case ',': case ';': // separator char has beed found return new String(chars, beg, end - beg); case '\\': // escaped char chars[end++] = getEscaped(); pos++; break; case ' ': // need to figure out whether space defines // the end of attribute value or not int cur = end; pos++; chars[end++] = ' '; for (; pos < chars.length && chars[pos] == ' '; pos++) { chars[end++] = ' '; } if (pos == chars.length || chars[pos] == ',' || chars[pos] == '+' || chars[pos] == ';') { // separator char or the end of DN has beed found return new String(chars, beg, cur - beg); } break; default: chars[end++] = chars[pos]; pos++; } } } /** * Returns an escaped char */ private char getEscaped() throws IOException { pos++; if (pos == chars.length) { throw new IOException("Invalid distinguished name string"); } char ch = chars[pos]; switch (ch) { case '"': case '\\': hasQE = true; return ch; case ',': case '=': case '+': case '<': case '>': case '#': case ';': case ' ': case '*': case '%': case '_': //FIXME: escaping is allowed only for leading or trailing space char return ch; default: // RFC doesn't explicitly say that escaped hex pair is // interpreted as UTF-8 char. It only contains an example of such DN. return getUTF8(); } } /** * Decodes a UTF-8 char. */ protected char getUTF8() throws IOException { int res = getByte(pos); pos++; //FIXME tmp if (res < 128) { // one byte: 0-7F return (char) res; } else if (res >= 192 && res <= 247) { int count; if (res <= 223) { // two bytes: C0-DF count = 1; res = res & 0x1F; } else if (res <= 239) { // three bytes: E0-EF count = 2; res = res & 0x0F; } else { // four bytes: F0-F7 count = 3; res = res & 0x07; } int b; for (int i = 0; i < count; i++) { pos++; if (pos == chars.length || chars[pos] != '\\') { return 0x3F; //FIXME failed to decode UTF-8 char - return '?' } pos++; b = getByte(pos); pos++; //FIXME tmp if ((b & 0xC0) != 0x80) { return 0x3F; //FIXME failed to decode UTF-8 char - return '?' } res = (res << 6) + (b & 0x3F); } return (char) res; } else { return 0x3F; //FIXME failed to decode UTF-8 char - return '?' } } /** * Returns byte representation of a char pair * The char pair is composed of DN char in * specified 'position' and the next char * According to BNF syntax: * hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" * / "a" / "b" / "c" / "d" / "e" / "f" */ private int getByte(int position) throws IOException { if ((position + 1) >= chars.length) { // to avoid ArrayIndexOutOfBoundsException throw new IOException("Invalid distinguished name string"); } int b1 = chars[position]; if (b1 >= '0' && b1 <= '9') { b1 = b1 - '0'; } else if (b1 >= 'a' && b1 <= 'f') { b1 = b1 - 87; // 87 = 'a' - 10 } else if (b1 >= 'A' && b1 <= 'F') { b1 = b1 - 55; // 55 = 'A' - 10 } else { throw new IOException("Invalid distinguished name string"); } int b2 = chars[position + 1]; if (b2 >= '0' && b2 <= '9') { b2 = b2 - '0'; } else if (b2 >= 'a' && b2 <= 'f') { b2 = b2 - 87; // 87 = 'a' - 10 } else if (b2 >= 'A' && b2 <= 'F') { b2 = b2 - 55; // 55 = 'A' - 10 } else { throw new IOException("Invalid distinguished name string"); } return (b1 << 4) + b2; } /** * Parses DN * * @return a list of Relative Distinguished Names(RND), * each RDN is represented as a list of AttributeTypeAndValue objects */ public List> parse() throws IOException { List> list = new ArrayList>(); String attType = nextAT(); if (attType == null) { return list; //empty list of RDNs } List atav = new ArrayList(); while (true) { if (pos == chars.length) { //empty Attribute Value atav.add(new AttributeTypeAndValue(attType, new AttributeValue("", false))); list.add(0, atav); return list; } switch (chars[pos]) { case '"': atav.add(new AttributeTypeAndValue(attType, new AttributeValue(quotedAV(), hasQE))); break; case '#': atav.add(new AttributeTypeAndValue(attType, new AttributeValue(hexAV(), encoded))); break; case '+': case ',': case ';': // compatibility with RFC 1779: semicolon can separate RDNs //empty attribute value atav.add(new AttributeTypeAndValue(attType, new AttributeValue("", false))); break; default: atav.add(new AttributeTypeAndValue(attType, new AttributeValue( escapedAV(), hasQE))); } if (pos >= chars.length) { list.add(0, atav); return list; } if (chars[pos] == ',' || chars[pos] == ';') { list.add(0, atav); atav = new ArrayList(); } else if (chars[pos] != '+') { throw new IOException("Invalid distinguished name string"); } pos++; attType = nextAT(); if (attType == null) { throw new IOException("Invalid distinguished name string"); } } } }