// --- BEGIN COPYRIGHT BLOCK --- // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; version 2 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // // (C) 2007 Red Hat, Inc. // All rights reserved. // --- END COPYRIGHT BLOCK --- package netscape.security.x509; import java.io.ByteArrayOutputStream; import java.io.CharArrayWriter; import java.io.IOException; import java.io.PushbackReader; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.util.Vector; import netscape.security.util.DerValue; import netscape.security.util.ObjectIdentifier; import sun.io.ByteToCharConverter; /** * A converter that converts Ldap v3 DN strings as specified in * draft-ietf-asid-ldapv3-dn-03.txt to a X500Name, RDN or AVA and * vice versa. * * @see LdapDNStrConverter * @see X500Name * @see RDN * @see AVA * @see X500NameAttrMap * * @author Lily Hsiao, Slava Galperin at Netscape Communications, Inc. */ public class LdapV3DNStrConverter extends LdapDNStrConverter { // // Constructors // /** * Constructs a LdapV3DNStrConverter using the global default * X500NameAttrMap and accept OIDs not in the default X500NameAttrMap. * @see X500NameAttrMap */ public LdapV3DNStrConverter() { attrMap = X500NameAttrMap.getDefault(); acceptUnknownOids = true; } /** * Constructs a LdapV3DNStrConverter using the specified X500NameAttrMap * and a boolean indicating whether to accept OIDs not listed in the * X500NameAttrMap. * * @param attributeMap a X500NameAttrMap * @param doAcceptUnknownOids whether to convert unregistered OIDs * (oids not in the X500NameAttrMap) * @see X500NameAttrMap */ public LdapV3DNStrConverter(X500NameAttrMap attributeMap, boolean doAcceptUnknownOids) { attrMap = attributeMap; acceptUnknownOids = doAcceptUnknownOids; } // // public parsing methods // From LdapDNStrConverter interface // /** * Parse a Ldap v3 DN string to a X500Name. * * @param dn a LDAP v3 DN String * @return a X500Name * @exception IOException if an error occurs during the conversion. */ public X500Name parseDN(String dn) throws IOException { return parseDN(dn, null); } /** * Like parseDN(String) with a DER encoding order given as argument for * Directory Strings. */ public X500Name parseDN(String dn, byte[] encodingOrder) throws IOException { StringReader dn_reader = new StringReader(dn); PushbackReader in = new PushbackReader(dn_reader, 5); Vector rdnVector = new Vector(); RDN[] names; return parseDN(in, encodingOrder); } /** * Parse a Ldap v3 DN string with a RDN component to a RDN * * @param rdn a LDAP v3 DN String * @return a RDN * @exception IOException if an error occurs during the conversion. */ public RDN parseRDN(String rdn) throws IOException { return parseRDN(rdn, null); } /** * Like parseRDN(String) with a DER encoding order given as argument for * Directory Strings. */ public RDN parseRDN(String rdn, byte[] encodingOrder) throws IOException { StringReader rdn_reader = new StringReader(rdn); PushbackReader in = new PushbackReader(rdn_reader, 5); Vector avaVector = new Vector(); AVA[] assertion; return parseRDN(in, null); } /** * Parse a Ldap v3 DN string with a AVA component to a AVA. * * @param ava a LDAP v3 DN string * @return a AVA */ public AVA parseAVA(String ava) throws IOException { return parseAVA(ava, null); } /** * Like parseDN(String) with a DER encoding order given as argument for * Directory Strings. */ public AVA parseAVA(String ava, byte[] encodingOrder) throws IOException { StringReader ava_reader = new StringReader(ava); PushbackReader in = new PushbackReader(ava_reader, 5); return parseAVA(in, encodingOrder); } // // public parsing methods called by other methods. // /** * Parses a Ldap DN string in a string reader to a X500Name. * * @param in Pushback string reader for a Ldap DN string. * The pushback reader must have a pushback buffer size > 2. * * @return a X500Name * * @exception IOException if any reading or parsing error occurs. */ public X500Name parseDN(PushbackReader in) throws IOException { return parseDN(in, null); } /** * Like parseDN(PushbackReader in) with a DER encoding order given as * argument for Directory Strings. */ public X500Name parseDN(PushbackReader in, byte[] encodingOrder) throws IOException { RDN rdn; int lastChar; Vector rdnVector = new Vector(); RDN names[]; int i,j; do { rdn = parseRDN(in, encodingOrder); rdnVector.addElement(rdn); lastChar = in.read(); } while (lastChar == ',' || lastChar == ';'); names = new RDN[rdnVector.size()]; for (i=0, j=rdnVector.size()-1; i < rdnVector.size(); i++, j--) names[j] = (RDN)rdnVector.elementAt(i); return new X500Name(names); } /** * Parses Ldap DN string with a rdn component * from a string reader to a RDN. The string reader will point * to the separator after the rdn component or -1 if at end of string. * * @param in Pushback string reader containing a Ldap DN string with * at least one rdn component. * The pushback reader must have a pushback buffer size > 2. * * @return RDN object of the first rdn component in the Ldap DN string. * * @exception IOException if any read or parse error occurs. */ public RDN parseRDN(PushbackReader in) throws IOException { return parseRDN(in, null); } /** * Like parseRDN(PushbackReader) with a DER encoding order given as * argument for Directory Strings. */ public RDN parseRDN(PushbackReader in, byte[] encodingOrder) throws IOException { Vector avaVector = new Vector(); AVA ava; int lastChar; AVA assertion[]; do { ava = parseAVA(in, encodingOrder); avaVector.addElement(ava); lastChar = in.read(); } while (lastChar == '+' ); if (lastChar != -1) in.unread(lastChar); assertion = new AVA[avaVector.size()]; for (int i = 0; i < avaVector.size(); i++) assertion[i] = (AVA)avaVector.elementAt(i); return new RDN(assertion); } /** * Parses a Ldap DN string with a AVA component * from a string reader to an AVA. The string reader will point * to the AVA separator after the ava string or -1 if end of string. * * @param in a Pushback reader containg a Ldap string with * at least one AVA component. * The Pushback reader must have a pushback buffer size > 2. * * @return AVA object of the first AVA component in the Ldap DN string. */ public AVA parseAVA(PushbackReader in) throws IOException { return parseAVA(in, null); } /** * Like parseAVA(PushbackReader) with a DER encoding order given as * argument for Directory Strings. */ public AVA parseAVA(PushbackReader in, byte[] encodingOrder) throws IOException { int c; ObjectIdentifier oid; DerValue value; StringBuffer keywordBuf; StringBuffer valueBuf; ByteArrayOutputStream berStream; char hexChar1, hexChar2; CharArrayWriter hexCharsBuf; String endChars; /* First get the keyword indicating the attribute's type, * and map it to the appropriate OID. */ keywordBuf = new StringBuffer(); for (;;) { c = in.read (); if (c == '=') break; if (c == -1) { throw new IOException("Bad AVA format: Missing '='"); } keywordBuf.append ((char)c); } oid = parseAVAKeyword(keywordBuf.toString()); /* Now parse the value. "#hex", a quoted string, or a string * terminated by "+", ",", ";", ">". Whitespace before or after * the value is stripped. */ for (c = in.read (); c == ' '; c = in.read ()) continue; if (c == -1) throw new IOException("Bad AVA format: Missing attribute value"); if (c == '#') { /* * NOTE per LDAPv3 dn string ietf standard the value represented * by this form is a BER value. But we only support DER value here * which is only a form of BER. */ berStream = new ByteArrayOutputStream(); int b; for (;;) { hexChar1 = (char)(c = in.read()); if (c == -1 || octoEndChars.indexOf(c) > 0) // end of value break; hexChar2 = (char)(c = in.read()); if (hexDigits.indexOf(hexChar1) == -1 || hexDigits.indexOf(hexChar2) == -1) throw new IOException("Bad AVA value: bad hex value."); b = (Character.digit(hexChar1, 16)<<4) + Character.digit(hexChar2, 16); berStream.write(b); } if (berStream.size() == 0) throw new IOException("bad AVA format: invalid hex value"); value = parseAVAValue(berStream.toByteArray(), oid); while (c == ' ' && c != -1) c = in.read(); } else { valueBuf = new StringBuffer (); boolean quoted = false; if (c == '"') { quoted = true; endChars = quotedEndChars; if ((c = in.read()) == -1) throw new IOException("Bad AVA format: Missing attrValue"); } else { endChars = valueEndChars; } // QUOTATION * ( quotechar / pair ) QUOTATION // quotechar = any character except '\' or QUOTATION // pair = '\' ( special | '\' | QUOTATION | hexpair ) while (c != -1 && endChars.indexOf(c) == -1) { if (c == '\\') { if ((c = in.read()) == -1) throw new IOException("Bad AVA format: expecting " + "escaped char."); // expect escaping of special chars, space and CR. if (specialChars.indexOf((char)c) != -1 || c == '\n' || c == '\\' || c == '"' || c == ' ') { valueBuf.append((char)c); } else if (hexDigits.indexOf(c) != -1) { hexCharsBuf = new CharArrayWriter(); // handle sequence of '\' hexpair do { hexChar1 = (char)c; hexChar2 = (char)(c = in.read()); if (hexDigits.indexOf((char)c) == -1) throw new IOException("Bad AVA format: " + "invalid escaped hex pair"); hexCharsBuf.write(hexChar1); hexCharsBuf.write(hexChar2); // read ahead to next '\' hex-char if any. if ((c = in.read()) == -1) break; if (c != '\\') { in.unread(c); break; } if ((c = in.read()) == -1) throw new IOException("Bad AVA format: "+ "expecting escaped char."); if (hexDigits.indexOf((char)c) == -1) { in.unread(c); in.unread((int)'\\'); break; } } while (true); valueBuf.append( getStringFromHexpairs(hexCharsBuf.toCharArray())); } else { throw new IOException("Bad AVA format: " + "invalid escaping"); } } else valueBuf.append((char)c); c = in.read(); } value = parseAVAValue( valueBuf.toString().trim(), oid, encodingOrder); if (quoted) { // move to next non-white space do { c = in.read(); } while (c == ' '); if (c != -1 && valueEndChars.indexOf(c) == -1) throw new IOException( "Bad AVA format: separator expected at end of ava."); } } if (c != -1) in.unread(c); return new AVA(oid, value); } /** * Converts a AVA keyword from a Ldap DN string to an ObjectIdentifier * from the attribute map or, if this keyword is an OID not * in the attribute map, create a new ObjectIdentifier for the keyword * if acceptUnknownOids is true. * * @param avaKeyword AVA keyword from a Ldap DN string. * * @return a ObjectIdentifier object * @exception IOException if the keyword is an OID not in the attribute * map and acceptUnknownOids is false, or * if an error occurs during conversion. */ public ObjectIdentifier parseAVAKeyword(String avaKeyword) throws IOException { String keyword = avaKeyword.toUpperCase().trim(); String oid_str = null; ObjectIdentifier oid, new_oid; if (Character.digit(keyword.charAt(0), 10) != -1) { // value is an oid string of 1.2.3.4 oid_str = keyword; } else if (keyword.startsWith("oid.") || keyword.startsWith("OID.")) { // value is an oid string of oid.1.2.3.4 or OID.1.2... oid_str = keyword.substring(4); } if (oid_str != null) { // value is an oid string of 1.2.3.4 or oid.1.2.3.4 or OID.1.2... new_oid = new ObjectIdentifier(oid_str); oid = attrMap.getOid(new_oid); if (oid == null) { if (!acceptUnknownOids) throw new IOException("Unknown AVA OID."); oid = new_oid; } } else { oid = attrMap.getOid(keyword); if (oid == null) throw new IOException("Unknown AVA keyword '"+keyword+"'."); } return oid; } /** * Converts a AVA value from a Ldap dn string to a * DerValue according the attribute type. For example, a value for * CN, OU or O is expected to be a Directory String and will be converted * to a DerValue of ASN.1 type PrintableString, T61String or * UniversalString. A Directory String is a ASN.1 CHOICE of Printable, * T.61 or Universal string. * * @param avaValueString a attribute value from a Ldap DN string. * @param oid OID of the attribute. * * @return DerValue for the value. * * @exception IOException if an error occurs during conversion. * @see AVAValueConverter */ public DerValue parseAVAValue(String avaValueString, ObjectIdentifier oid) throws IOException { return parseAVAValue(avaValueString, oid, null); } /** * Like parseAVAValue(String) with a DER encoding order given as argument * for Directory Strings. */ public DerValue parseAVAValue( String avaValueString, ObjectIdentifier oid, byte[] encodingOrder) throws IOException { AVAValueConverter valueConverter = attrMap.getValueConverter(oid); if (valueConverter == null) { if (!acceptUnknownOids) { throw new IllegalArgumentException( "Unrecognized OID for AVA value conversion"); } else { valueConverter = new GenericValueConverter(); } } return valueConverter.getValue(avaValueString, encodingOrder); } /** * Converts a value in BER encoding, for example given in octothorpe form * in a Ldap v3 dn string, to a DerValue. Checks if the BER encoded value * is a legal value for the attribute.

* NOTE: only DER encoded values are supported for * the BER encoded value. * * @param berValue a value in BER encoding * @param oid ObjectIdentifier of the attribute. * * @return DerValue for the BER encoded value * @exception IOException if an error occurs during conversion. */ public DerValue parseAVAValue(byte[] berValue, ObjectIdentifier oid) throws IOException { AVAValueConverter valueConverter = attrMap.getValueConverter(oid); if (valueConverter == null && !acceptUnknownOids) { throw new IllegalArgumentException( "Unrecognized OID for AVA value conversion"); } else { valueConverter = new GenericValueConverter(); } return valueConverter.getValue(berValue); } // // public encoding methods. // /** * Converts a X500Name object to a Ldap v3 DN string (except in unicode). * * @param x500name a X500Name * * @return a Ldap v3 DN String (except in unicode). * * @exception IOException if an error is encountered during conversion. */ public String encodeDN(X500Name x500name) throws IOException { RDN[] rdns = x500name.getNames(); // String fullname = null; StringBuffer fullname = new StringBuffer(); String s; int i; if (rdns.length == 0) return ""; i = rdns.length-1; fullname.append(encodeRDN(rdns[i--])); while (i >= 0) { s = encodeRDN(rdns[i--]); fullname.append(","); fullname.append(s); }; return fullname.toString(); } /** * Converts a RDN to a Ldap v3 DN string (except in unicode). * * @param rdn a RDN * * @return a LDAP v3 DN string (except in unicode). * * @exception IOException if an error is encountered during conversion. */ public String encodeRDN(RDN rdn) throws IOException { AVA[] avas = rdn.getAssertion(); // String relname = null; StringBuffer relname = new StringBuffer(); String s; int i=0; relname.append(encodeAVA(avas[i++])); while (i > 4) & 0x0f)); retval.append (hexDigits.charAt (data [i] & 0x0f)); } } else { retval.append(encodeString(temp)); } } catch (IOException e) { throw new IllegalArgumentException ("malformed AVA DER Value"); } return retval.toString (); } /** * converts a raw value string to a string in Ldap V3 DN string format. * @param valueStr a 'raw' value string. * @return a attribute value string in Ldap V3 DN string format. */ public String encodeString(String valueStr) { int i,j; int len; StringBuffer retval = new StringBuffer(); /* * generate string according to ldapv3 DN. escaping is used. * Strings generated this way are acceptable by rfc1779 * implementations. */ len = valueStr.length (); // get index of first space at the end of the string. for (j = len-1; j >= 0 && valueStr.charAt(j) == ' '; j--) continue; // escape spaces at the beginning of the string. for (i = 0; i <= j && valueStr.charAt(i) == ' '; i++) { retval.append('\\'); retval.append(valueStr.charAt(i)); } // escape special characters in the middle of the string. for ( ; i <= j ; i++) { if (valueStr.charAt(i) == '\\') { retval.append('\\'); retval.append(valueStr.charAt(i)); } else if (specialChars.indexOf(valueStr.charAt(i)) != -1) { retval.append('\\'); retval.append(valueStr.charAt(i)); } else if (valueStr.charAt(i) == '"') { retval.append('\\'); retval.append(valueStr.charAt(i)); } else retval.append(valueStr.charAt(i)); } // esacape spaces at the end. for ( ; i < valueStr.length(); i++) { retval.append('\\'); retval.append(' '); } return retval.toString(); } // // public get/set methods // /** * gets the X500NameAttrMap used by the converter. * @return X500NameAttrMap used by this converter. */ public X500NameAttrMap getAttrMap() { return attrMap; } /** * returns true if the converter accepts unregistered attributes i.e. * OIDS not in the X500NameAttrMap. * @return true if converter converts attributes not in the * X500NameAttrMap. */ public boolean getAcceptUnknownOids() { return acceptUnknownOids; } // // private and protected variables // protected X500NameAttrMap attrMap; protected boolean acceptUnknownOids; // // private and protected static variables & methods. // protected static final String specialChars = ",+=<>#;"; protected static final String valueEndChars = "+,;>"; protected static final String quotedEndChars = "\""; protected static final String octoEndChars = " " + valueEndChars; /* * Values that aren't printable strings are emitted as BER-encoded * hex data. */ protected static final String hexDigits = "0123456789ABCDEFabcdef"; /** * Parse a sequence of hex pairs, each pair a UTF8 byte to a java string. * For example, "4C75C48D" is "Luc", the last c with caron. */ protected static char[] getStringFromHexpairs(char[] hexPairs) throws UnsupportedEncodingException { ByteToCharConverter utf8_bcc; byte utf8_buf[]; char char_buf[]; int ret; int i,j; try { utf8_bcc = ByteToCharConverter.getConverter("UTF8"); } catch (UnsupportedEncodingException e) { throw new UnsupportedEncodingException( "No UTF8 byte to char converter to use for "+ "parsing LDAP DN String"); } utf8_bcc.setSubstitutionMode(false); utf8_buf = new byte[hexPairs.length/2]; char_buf = new char[utf8_buf.length*utf8_bcc.getMaxCharsPerByte()]; for ( i=0,j=0 ; i < hexPairs.length ; i++,j++ ) { utf8_buf[j] = (byte) ((Character.digit( hexPairs[i++], 16 ) << 4) + Character.digit( hexPairs[i], 16 )); } try { ret = utf8_bcc.convert( utf8_buf, 0, utf8_buf.length, char_buf, 0, char_buf.length ); } catch (java.io.CharConversionException e) { throw new IllegalArgumentException( "Invalid hex pair in LDAP DN String." ); } char [] out_buf = new char[ ret ]; System.arraycopy( char_buf, 0, out_buf, 0, ret ); return out_buf; } }