0001: /*
0002: * @(#)AVA.java 1.35 06/10/10
0003: *
0004: * Copyright 1990-2006 Sun Microsystems, Inc. All Rights Reserved.
0005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
0006: *
0007: * This program is free software; you can redistribute it and/or
0008: * modify it under the terms of the GNU General Public License version
0009: * 2 only, as published by the Free Software Foundation.
0010: *
0011: * This program is distributed in the hope that it will be useful, but
0012: * WITHOUT ANY WARRANTY; without even the implied warranty of
0013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0014: * General Public License version 2 for more details (a copy is
0015: * included at /legal/license.txt).
0016: *
0017: * You should have received a copy of the GNU General Public License
0018: * version 2 along with this work; if not, write to the Free Software
0019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
0020: * 02110-1301 USA
0021: *
0022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
0023: * Clara, CA 95054 or visit www.sun.com if you need additional
0024: * information or have any questions.
0025: *
0026: */
0027:
0028: package sun.security.x509;
0029:
0030: import java.io.ByteArrayOutputStream;
0031: import java.io.IOException;
0032: import java.io.OutputStream;
0033: import java.io.Reader;
0034:
0035: import java.util.Map;
0036: import java.util.HashMap;
0037: import java.util.ArrayList;
0038:
0039: import java.util.Locale;
0040: import sun.text.Normalizer;
0041:
0042: import sun.security.util.*;
0043: import sun.security.pkcs.PKCS9Attribute;
0044:
0045: /**
0046: * X.500 Attribute-Value-Assertion (AVA): an attribute, as identified by
0047: * some attribute ID, has some particular value. Values are as a rule ASN.1
0048: * printable strings. A conventional set of type IDs is recognized when
0049: * parsing (and generating) RFC 1779 or RFC 2253 syntax strings.
0050: *
0051: * <P>AVAs are components of X.500 relative names. Think of them as being
0052: * individual fields of a database record. The attribute ID is how you
0053: * identify the field, and the value is part of a particular record.
0054: * <p>
0055: * Note that instances of this class are immutable.
0056: *
0057: * @see X500Name
0058: * @see RDN
0059: *
0060: * @version 1.35, 10/10/06
0061: *
0062: * @author David Brownell
0063: * @author Amit Kapoor
0064: * @author Hemma Prafullchandra
0065: */
0066: public class AVA implements DerEncoder {
0067:
0068: private static final Debug debug = Debug.getInstance("x509",
0069: "\t[AVA]");
0070:
0071: /**
0072: * DEFAULT format allows both RFC1779 and RFC2253 syntax and
0073: * additional keywords.
0074: */
0075: final static int DEFAULT = 1;
0076: /**
0077: * RFC1779 specifies format according to RFC1779.
0078: */
0079: final static int RFC1779 = 2;
0080: /**
0081: * RFC2253 specifies format according to RFC2253.
0082: */
0083: final static int RFC2253 = 3;
0084:
0085: // currently not private, accessed directly from RDN
0086: final ObjectIdentifier oid;
0087: final DerValue value;
0088:
0089: /*
0090: * If the value has any of these characters in it, it must be quoted.
0091: * Backslash and quote characters must also be individually escaped.
0092: * Leading and trailing spaces, also multiple internal spaces, also
0093: * call for quoting the whole string.
0094: */
0095: private static final String specialChars = ",+=\n<>#;";
0096:
0097: /*
0098: * In RFC2253, if the value has any of these characters in it, it
0099: * must be quoted by a preceding \.
0100: */
0101: private static final String specialChars2253 = ",+\"\\<>;";
0102:
0103: /*
0104: * includes special chars from RFC1779 and RFC2253, as well as ' '
0105: */
0106: private static final String specialCharsAll = ",=\n+<>#;\\\" ";
0107:
0108: /*
0109: * Values that aren't printable strings are emitted as BER-encoded
0110: * hex data.
0111: */
0112: private static final String hexDigits = "0123456789ABCDEF";
0113:
0114: public AVA(ObjectIdentifier type, DerValue val) {
0115: if ((type == null) || (val == null)) {
0116: throw new NullPointerException();
0117: }
0118: oid = type;
0119: value = val;
0120: }
0121:
0122: /**
0123: * Parse an RFC 1779 or RFC 2253 style AVA string: CN=fee fie foe fum
0124: * or perhaps with quotes. Not all defined AVA tags are supported;
0125: * of current note are X.400 related ones (PRMD, ADMD, etc).
0126: *
0127: * This terminates at unescaped AVA separators ("+") or RDN
0128: * separators (",", ";"), or DN terminators (">"), and removes
0129: * cosmetic whitespace at the end of values.
0130: */
0131: AVA(Reader in) throws IOException {
0132: this (in, DEFAULT);
0133: }
0134:
0135: /**
0136: * Parse an AVA string formatted according to format.
0137: *
0138: * NOTE: format RFC1779 should only allow RFC1779 syntax but is
0139: * actually DEFAULT with RFC1779 keywords.
0140: */
0141: AVA(Reader in, int format) throws IOException {
0142: // assume format is one of DEFAULT, RFC1779, RFC2253
0143:
0144: StringBuffer temp = new StringBuffer();
0145: int c;
0146:
0147: /*
0148: * First get the keyword indicating the attribute's type,
0149: * and map it to the appropriate OID.
0150: */
0151: while (true) {
0152: c = readChar(in, "Incorrect AVA format");
0153: if (c == '=') {
0154: break;
0155: }
0156: temp.append((char) c);
0157: }
0158:
0159: oid = AVAKeyword.getOID(temp.toString(), format);
0160:
0161: /*
0162: * Now parse the value. "#hex", a quoted string, or a string
0163: * terminated by "+", ",", ";", ">". Whitespace before or after
0164: * the value is stripped away unless format is RFC2253.
0165: */
0166: temp.setLength(0);
0167: if (format == RFC2253) {
0168: // read next character
0169: c = in.read();
0170: if (c == ' ') {
0171: throw new IOException("Incorrect AVA RFC2253 format - "
0172: + "leading space must be escaped");
0173: }
0174: } else {
0175: // read next character skipping whitespace
0176: do {
0177: c = in.read();
0178: } while ((c == ' ') || (c == '\n'));
0179: }
0180: if (c == -1) {
0181: // empty value
0182: value = new DerValue("");
0183: return;
0184: }
0185:
0186: if (c == '#') {
0187: value = parseHexString(in, format);
0188: } else if ((c == '"') && (format != RFC2253)) {
0189: value = parseQuotedString(in, temp);
0190: } else {
0191: value = parseString(in, c, format, temp);
0192: }
0193: }
0194:
0195: /**
0196: * Get the ObjectIdentifier of this AVA.
0197: */
0198: public ObjectIdentifier getObjectIdentifier() {
0199: return oid;
0200: }
0201:
0202: /**
0203: * Get the value of this AVA as a DerValue.
0204: */
0205: public DerValue getDerValue() {
0206: return value;
0207: }
0208:
0209: /**
0210: * Get the value of this AVA as a String.
0211: *
0212: * @exception RuntimeException if we could not obtain the string form
0213: * (should not occur)
0214: */
0215: public String getValueString() {
0216: try {
0217: String s = value.getAsString();
0218: if (s == null) {
0219: throw new RuntimeException("AVA string is null");
0220: }
0221: return s;
0222: } catch (IOException e) {
0223: // should not occur
0224: throw new RuntimeException("AVA error: " + e, e);
0225: }
0226: }
0227:
0228: private static DerValue parseHexString(Reader in, int format)
0229: throws IOException {
0230:
0231: int c;
0232: ByteArrayOutputStream baos = new ByteArrayOutputStream();
0233: byte b = 0;
0234: int cNdx = 0;
0235: while (true) {
0236: c = in.read();
0237:
0238: if (isTerminator(c, format)) {
0239: break;
0240: }
0241:
0242: int cVal = hexDigits.indexOf(Character
0243: .toUpperCase((char) c));
0244:
0245: if (cVal == -1) {
0246: throw new IOException("AVA parse, invalid hex "
0247: + "digit: " + (char) c);
0248: }
0249:
0250: if ((cNdx % 2) == 1) {
0251: b = (byte) ((b * 16) + (byte) (cVal));
0252: baos.write(b);
0253: } else {
0254: b = (byte) (cVal);
0255: }
0256: cNdx++;
0257: }
0258:
0259: // throw exception if no hex digits
0260: if (cNdx == 0) {
0261: throw new IOException("AVA parse, zero hex digits");
0262: }
0263:
0264: // throw exception if odd number of hex digits
0265: if (cNdx % 2 == 1) {
0266: throw new IOException("AVA parse, odd number of hex digits");
0267: }
0268:
0269: return new DerValue(baos.toByteArray());
0270: }
0271:
0272: private DerValue parseQuotedString(Reader in, StringBuffer temp)
0273: throws IOException {
0274:
0275: // RFC1779 specifies that an entire RDN may be enclosed in double
0276: // quotes. In this case the syntax is any sequence of
0277: // backslash-specialChar, backslash-backslash,
0278: // backslash-doublequote, or character other than backslash or
0279: // doublequote.
0280: int c = readChar(in, "Quoted string did not end in quote");
0281:
0282: ArrayList embeddedHex = new ArrayList();
0283: boolean isPrintableString = true;
0284: while (c != '"') {
0285: if (c == '\\') {
0286: c = readChar(in, "Quoted string did not end in quote");
0287:
0288: // check for embedded hex pairs
0289: Byte hexByte = null;
0290: if ((hexByte = getEmbeddedHexPair(c, in)) != null) {
0291:
0292: // always encode AVAs with embedded hex as UTF8
0293: isPrintableString = false;
0294:
0295: // append consecutive embedded hex
0296: // as single string later
0297: embeddedHex.add(hexByte);
0298: c = in.read();
0299: continue;
0300: }
0301:
0302: if (c != '\\' && c != '"'
0303: && specialChars.indexOf((char) c) < 0) {
0304: throw new IOException(
0305: "Invalid escaped character in AVA: "
0306: + (char) c);
0307: }
0308: }
0309:
0310: // add embedded hex bytes before next char
0311: if (embeddedHex.size() > 0) {
0312: String hexString = getEmbeddedHexString(embeddedHex);
0313: temp.append(hexString);
0314: embeddedHex.clear();
0315: }
0316:
0317: // check for non-PrintableString chars
0318: isPrintableString &= DerValue
0319: .isPrintableStringChar((char) c);
0320: temp.append((char) c);
0321: c = readChar(in, "Quoted string did not end in quote");
0322: }
0323:
0324: // add trailing embedded hex bytes
0325: if (embeddedHex.size() > 0) {
0326: String hexString = getEmbeddedHexString(embeddedHex);
0327: temp.append(hexString);
0328: embeddedHex.clear();
0329: }
0330:
0331: do {
0332: c = in.read();
0333: } while ((c == '\n') || (c == ' '));
0334: if (c != -1) {
0335: throw new IOException("AVA had characters other than "
0336: + "whitespace after terminating quote");
0337: }
0338:
0339: // encode as PrintableString unless value contains
0340: // non-PrintableString chars
0341: if (this .oid.equals(PKCS9Attribute.EMAIL_ADDRESS_OID)) {
0342: // EmailAddress must be IA5String
0343: return new DerValue(DerValue.tag_IA5String, temp.toString()
0344: .trim());
0345: } else if (isPrintableString) {
0346: return new DerValue(temp.toString().trim());
0347: } else {
0348: return new DerValue(DerValue.tag_UTF8String, temp
0349: .toString().trim());
0350: }
0351: }
0352:
0353: private DerValue parseString(Reader in, int c, int format,
0354: StringBuffer temp) throws IOException {
0355:
0356: ArrayList embeddedHex = new ArrayList();
0357: boolean isPrintableString = true;
0358: boolean escape = false;
0359: boolean leadingChar = true;
0360: int spaceCount = 0;
0361: do {
0362: escape = false;
0363: if (c == '\\') {
0364: escape = true;
0365: c = readChar(in, "Invalid trailing backslash");
0366:
0367: // check for embedded hex pairs
0368: Byte hexByte = null;
0369: if ((hexByte = getEmbeddedHexPair(c, in)) != null) {
0370:
0371: // always encode AVAs with embedded hex as UTF8
0372: isPrintableString = false;
0373:
0374: // append consecutive embedded hex
0375: // as single string later
0376: embeddedHex.add(hexByte);
0377: c = in.read();
0378: leadingChar = false;
0379: continue;
0380: }
0381:
0382: // check if character was improperly escaped
0383: if ((format == DEFAULT && specialCharsAll
0384: .indexOf((char) c) == -1)
0385: || (format == RFC1779
0386: && specialChars.indexOf((char) c) == -1
0387: && c != '\\' && c != '\"')) {
0388:
0389: throw new IOException(
0390: "Invalid escaped character in AVA: '"
0391: + (char) c + "'");
0392:
0393: } else if (format == RFC2253) {
0394: if (c == ' ') {
0395: // only leading/trailing space can be escaped
0396: if (!leadingChar && !trailingSpace(in)) {
0397: throw new IOException(
0398: "Invalid escaped space character "
0399: + "in AVA. Only a leading or trailing "
0400: + "space character can be escaped.");
0401: }
0402: } else if (c == '#') {
0403: // only leading '#' can be escaped
0404: if (!leadingChar) {
0405: throw new IOException(
0406: "Invalid escaped '#' character in AVA. "
0407: + "Only a leading '#' can be escaped.");
0408: }
0409: } else if (specialChars2253.indexOf((char) c) == -1) {
0410: throw new IOException(
0411: "Invalid escaped character in AVA: '"
0412: + (char) c + "'");
0413:
0414: }
0415: }
0416:
0417: } else {
0418: // check if character should have been escaped
0419: if (format == RFC2253) {
0420: if (specialChars2253.indexOf((char) c) != -1) {
0421: throw new IOException("Character '" + (char) c
0422: + "' in AVA appears without escape");
0423: }
0424: }
0425: }
0426:
0427: // add embedded hex bytes before next char
0428: if (embeddedHex.size() > 0) {
0429: // add space(s) before embedded hex bytes
0430: for (int i = 0; i < spaceCount; i++) {
0431: temp.append(" ");
0432: }
0433: spaceCount = 0;
0434:
0435: String hexString = getEmbeddedHexString(embeddedHex);
0436: temp.append(hexString);
0437: embeddedHex.clear();
0438: }
0439:
0440: // check for non-PrintableString chars
0441: isPrintableString &= DerValue
0442: .isPrintableStringChar((char) c);
0443: if (c == ' ' && escape == false) {
0444: // do not add non-escaped spaces yet
0445: // (non-escaped trailing spaces are ignored)
0446: spaceCount++;
0447: } else {
0448: // add space(s)
0449: for (int i = 0; i < spaceCount; i++) {
0450: temp.append(" ");
0451: }
0452: spaceCount = 0;
0453: temp.append((char) c);
0454: }
0455: c = in.read();
0456: leadingChar = false;
0457: } while (isTerminator(c, format) == false);
0458:
0459: if (format == RFC2253 && spaceCount > 0) {
0460: throw new IOException("Incorrect AVA RFC2253 format - "
0461: + "trailing space must be escaped");
0462: }
0463:
0464: // add trailing embedded hex bytes
0465: if (embeddedHex.size() > 0) {
0466: String hexString = getEmbeddedHexString(embeddedHex);
0467: temp.append(hexString);
0468: embeddedHex.clear();
0469: }
0470:
0471: // encode as PrintableString unless value contains
0472: // non-PrintableString chars
0473: if (this .oid.equals(PKCS9Attribute.EMAIL_ADDRESS_OID)) {
0474: // EmailAddress must be IA5String
0475: return new DerValue(DerValue.tag_IA5String, temp.toString());
0476: } else if (isPrintableString) {
0477: return new DerValue(temp.toString());
0478: } else {
0479: return new DerValue(DerValue.tag_UTF8String, temp
0480: .toString());
0481: }
0482: }
0483:
0484: private static Byte getEmbeddedHexPair(int c1, Reader in)
0485: throws IOException {
0486:
0487: if (hexDigits.indexOf(Character.toUpperCase((char) c1)) >= 0) {
0488: int c2 = readChar(in, "unexpected EOF - "
0489: + "escaped hex value must include two valid digits");
0490:
0491: if (hexDigits.indexOf(Character.toUpperCase((char) c2)) >= 0) {
0492: int hi = Character.digit((char) c1, 16);
0493: int lo = Character.digit((char) c2, 16);
0494: return new Byte((byte) ((hi << 4) + lo));
0495: } else {
0496: throw new IOException(
0497: "escaped hex value must include two valid digits");
0498: }
0499: }
0500: return null;
0501: }
0502:
0503: private static String getEmbeddedHexString(ArrayList hexList)
0504: throws IOException {
0505: byte[] hexBytes = new byte[hexList.size()];
0506: for (int i = 0; i < hexList.size(); i++) {
0507: hexBytes[i] = ((Byte) hexList.get(i)).byteValue();
0508: }
0509: return new String(hexBytes, "UTF8");
0510: }
0511:
0512: private static boolean isTerminator(int ch, int format) {
0513: switch (ch) {
0514: case -1:
0515: case '+':
0516: case ',':
0517: return true;
0518: case ';':
0519: case '>':
0520: return format != RFC2253;
0521: default:
0522: return false;
0523: }
0524: }
0525:
0526: private static int readChar(Reader in, String errMsg)
0527: throws IOException {
0528: int c = in.read();
0529: if (c == -1) {
0530: throw new IOException(errMsg);
0531: }
0532: return c;
0533: }
0534:
0535: private static boolean trailingSpace(Reader in) throws IOException {
0536:
0537: boolean trailing = false;
0538:
0539: if (!in.markSupported()) {
0540: // oh well
0541: return true;
0542: } else {
0543: // make readAheadLimit huge -
0544: // in practice, AVA was passed a StringReader from X500Name,
0545: // and StringReader ignores readAheadLimit anyways
0546: in.mark(9999);
0547: while (true) {
0548: int nextChar = in.read();
0549: if (nextChar == -1) {
0550: trailing = true;
0551: break;
0552: } else if (nextChar == ' ') {
0553: continue;
0554: } else if (nextChar == '\\') {
0555: int followingChar = in.read();
0556: if (followingChar != ' ') {
0557: trailing = false;
0558: break;
0559: }
0560: } else {
0561: trailing = false;
0562: break;
0563: }
0564: }
0565:
0566: in.reset();
0567: return trailing;
0568: }
0569: }
0570:
0571: AVA(DerValue derval) throws IOException {
0572: // Individual attribute value assertions are SEQUENCE of two values.
0573: // That'd be a "struct" outside of ASN.1.
0574: if (derval.tag != DerValue.tag_Sequence) {
0575: throw new IOException("AVA not a sequence");
0576: }
0577: oid = X500Name.intern(derval.data.getOID());
0578: value = derval.data.getDerValue();
0579:
0580: if (derval.data.available() != 0) {
0581: throw new IOException("AVA, extra bytes = "
0582: + derval.data.available());
0583: }
0584: }
0585:
0586: AVA(DerInputStream in) throws IOException {
0587: this (in.getDerValue());
0588: }
0589:
0590: public boolean equals(Object obj) {
0591: if (this == obj) {
0592: return true;
0593: }
0594: if (obj instanceof AVA == false) {
0595: return false;
0596: }
0597: AVA other = (AVA) obj;
0598: return this .toRFC2253CanonicalString().equals(
0599: other.toRFC2253CanonicalString());
0600: }
0601:
0602: /**
0603: * Returns a hashcode for this AVA.
0604: *
0605: * @return a hashcode for this AVA.
0606: */
0607: public int hashCode() {
0608: return toRFC2253CanonicalString().hashCode();
0609: }
0610:
0611: /*
0612: * AVAs are encoded as a SEQUENCE of two elements.
0613: */
0614: public void encode(DerOutputStream out) throws IOException {
0615: derEncode(out);
0616: }
0617:
0618: /**
0619: * DER encode this object onto an output stream.
0620: * Implements the <code>DerEncoder</code> interface.
0621: *
0622: * @param out
0623: * the output stream on which to write the DER encoding.
0624: *
0625: * @exception IOException on encoding error.
0626: */
0627: public void derEncode(OutputStream out) throws IOException {
0628: DerOutputStream tmp = new DerOutputStream();
0629: DerOutputStream tmp2 = new DerOutputStream();
0630:
0631: tmp.putOID(oid);
0632: value.encode(tmp);
0633: tmp2.write(DerValue.tag_Sequence, tmp);
0634: out.write(tmp2.toByteArray());
0635: }
0636:
0637: private String toKeyword(int format) {
0638: return AVAKeyword.getKeyword(oid, format);
0639: }
0640:
0641: /**
0642: * Returns a printable form of this attribute, using RFC 1779
0643: * syntax for individual attribute/value assertions.
0644: */
0645: public String toString() {
0646: return toKeywordValueString(toKeyword(DEFAULT));
0647: }
0648:
0649: /**
0650: * Returns a printable form of this attribute, using RFC 1779
0651: * syntax for individual attribute/value assertions. It only
0652: * emits standardised keywords.
0653: */
0654: public String toRFC1779String() {
0655: return toKeywordValueString(toKeyword(RFC1779));
0656: }
0657:
0658: /**
0659: * Returns a printable form of this attribute, using RFC 2253
0660: * syntax for individual attribute/value assertions. It only
0661: * emits standardised keywords.
0662: */
0663: public String toRFC2253String() {
0664: /*
0665: * Section 2.3: The AttributeTypeAndValue is encoded as the string
0666: * representation of the AttributeType, followed by an equals character
0667: * ('=' ASCII 61), followed by the string representation of the
0668: * AttributeValue. The encoding of the AttributeValue is given in
0669: * section 2.4.
0670: */
0671: StringBuffer typeAndValue = new StringBuffer(100);
0672: typeAndValue.append(toKeyword(RFC2253));
0673: typeAndValue.append('=');
0674:
0675: /*
0676: * Section 2.4: Converting an AttributeValue from ASN.1 to a String.
0677: * If the AttributeValue is of a type which does not have a string
0678: * representation defined for it, then it is simply encoded as an
0679: * octothorpe character ('#' ASCII 35) followed by the hexadecimal
0680: * representation of each of the bytes of the BER encoding of the X.500
0681: * AttributeValue. This form SHOULD be used if the AttributeType is of
0682: * the dotted-decimal form.
0683: */
0684: if ((typeAndValue.charAt(0) >= '0' && typeAndValue.charAt(0) <= '9')
0685: || !isDerString(value, false)) {
0686: byte[] data = null;
0687: try {
0688: data = value.toByteArray();
0689: } catch (IOException ie) {
0690: throw new IllegalArgumentException(
0691: "DER Value conversion");
0692: }
0693: typeAndValue.append('#');
0694: for (int j = 0; j < data.length; j++) {
0695: byte b = data[j];
0696: typeAndValue.append(Character.forDigit(0xF & (b >>> 4),
0697: 16));
0698: typeAndValue.append(Character.forDigit(0xF & b, 16));
0699: }
0700: } else {
0701: /*
0702: * 2.4 (cont): Otherwise, if the AttributeValue is of a type which
0703: * has a string representation, the value is converted first to a
0704: * UTF-8 string according to its syntax specification.
0705: *
0706: * NOTE: this implementation only emits DirectoryStrings of the
0707: * types returned by isDerString().
0708: */
0709: String valStr = null;
0710: try {
0711: valStr = new String(value.getDataBytes(), "UTF8");
0712: } catch (IOException ie) {
0713: throw new IllegalArgumentException(
0714: "DER Value conversion");
0715: }
0716:
0717: /*
0718: * 2.4 (cont): If the UTF-8 string does not have any of the
0719: * following characters which need escaping, then that string can be
0720: * used as the string representation of the value.
0721: *
0722: * o a space or "#" character occurring at the beginning of the
0723: * string
0724: * o a space character occurring at the end of the string
0725: * o one of the characters ",", "+", """, "\", "<", ">" or ";"
0726: *
0727: * Implementations MAY escape other characters.
0728: *
0729: * NOTE: this implementation also recognizes "=" and "#" as
0730: * characters which need escaping.
0731: *
0732: * If a character to be escaped is one of the list shown above, then
0733: * it is prefixed by a backslash ('\' ASCII 92).
0734: *
0735: * Otherwise the character to be escaped is replaced by a backslash
0736: * and two hex digits, which form a single byte in the code of the
0737: * character.
0738: */
0739: final String escapees = ",=+<>#;\"\\";
0740: StringBuffer sbuffer = new StringBuffer();
0741:
0742: for (int i = 0; i < valStr.length(); i++) {
0743: char c = valStr.charAt(i);
0744: if (DerValue.isPrintableStringChar(c)
0745: || escapees.indexOf(c) >= 0) {
0746:
0747: // escape escapees
0748: if (escapees.indexOf(c) >= 0) {
0749: sbuffer.append('\\');
0750: }
0751:
0752: // append printable/escaped char
0753: sbuffer.append(c);
0754:
0755: } else if (debug != null && Debug.isOn("ava")) {
0756:
0757: // embed non-printable/non-escaped char
0758: // as escaped hex pairs for debugging
0759: byte[] valueBytes = null;
0760: try {
0761: valueBytes = Character.toString(c).getBytes(
0762: "UTF8");
0763: } catch (IOException ie) {
0764: throw new IllegalArgumentException(
0765: "DER Value conversion");
0766: }
0767: for (int j = 0; j < valueBytes.length; j++) {
0768: sbuffer.append('\\');
0769: char hexChar = Character.forDigit(
0770: 0xF & (valueBytes[j] >>> 4), 16);
0771: sbuffer.append(Character.toUpperCase(hexChar));
0772: hexChar = Character.forDigit(
0773: 0xF & (valueBytes[j]), 16);
0774: sbuffer.append(Character.toUpperCase(hexChar));
0775: }
0776: } else {
0777:
0778: // append non-printable/non-escaped char
0779: sbuffer.append(c);
0780: }
0781: }
0782:
0783: char[] chars = sbuffer.toString().toCharArray();
0784: sbuffer = new StringBuffer();
0785:
0786: // Find leading and trailing whitespace.
0787: int lead; // index of first char that is not leading whitespace
0788: for (lead = 0; lead < chars.length; lead++) {
0789: if (chars[lead] != ' ' && chars[lead] != '\r') {
0790: break;
0791: }
0792: }
0793: int trail; // index of last char that is not trailing whitespace
0794: for (trail = chars.length - 1; trail >= 0; trail--) {
0795: if (chars[trail] != ' ' && chars[trail] != '\r') {
0796: break;
0797: }
0798: }
0799:
0800: // escape leading and trailing whitespace
0801: for (int i = 0; i < chars.length; i++) {
0802: char c = chars[i];
0803: if (i < lead || i > trail) {
0804: sbuffer.append('\\');
0805: }
0806: sbuffer.append(c);
0807: }
0808: typeAndValue.append(sbuffer.toString());
0809: }
0810: return new String(typeAndValue);
0811: }
0812:
0813: public String toRFC2253CanonicalString() {
0814: /*
0815: * Section 2.3: The AttributeTypeAndValue is encoded as the string
0816: * representation of the AttributeType, followed by an equals character
0817: * ('=' ASCII 61), followed by the string representation of the
0818: * AttributeValue. The encoding of the AttributeValue is given in
0819: * section 2.4.
0820: */
0821: StringBuffer typeAndValue = new StringBuffer(40);
0822: typeAndValue.append(toKeyword(RFC2253));
0823: typeAndValue.append('=');
0824:
0825: /*
0826: * Section 2.4: Converting an AttributeValue from ASN.1 to a String.
0827: * If the AttributeValue is of a type which does not have a string
0828: * representation defined for it, then it is simply encoded as an
0829: * octothorpe character ('#' ASCII 35) followed by the hexadecimal
0830: * representation of each of the bytes of the BER encoding of the X.500
0831: * AttributeValue. This form SHOULD be used if the AttributeType is of
0832: * the dotted-decimal form.
0833: */
0834: if ((typeAndValue.charAt(0) >= '0' && typeAndValue.charAt(0) <= '9')
0835: || !isDerString(value, true)) {
0836: byte[] data = null;
0837: try {
0838: data = value.toByteArray();
0839: } catch (IOException ie) {
0840: throw new IllegalArgumentException(
0841: "DER Value conversion");
0842: }
0843: typeAndValue.append('#');
0844: for (int j = 0; j < data.length; j++) {
0845: byte b = data[j];
0846: typeAndValue.append(Character.forDigit(0xF & (b >>> 4),
0847: 16));
0848: typeAndValue.append(Character.forDigit(0xF & b, 16));
0849: }
0850: } else {
0851: /*
0852: * 2.4 (cont): Otherwise, if the AttributeValue is of a type which
0853: * has a string representation, the value is converted first to a
0854: * UTF-8 string according to its syntax specification.
0855: *
0856: * NOTE: this implementation only emits DirectoryStrings of the
0857: * types returned by isDerString().
0858: */
0859: String valStr = null;
0860: try {
0861: valStr = new String(value.getDataBytes(), "UTF8");
0862: } catch (IOException ie) {
0863: throw new IllegalArgumentException(
0864: "DER Value conversion");
0865: }
0866:
0867: /*
0868: * 2.4 (cont): If the UTF-8 string does not have any of the
0869: * following characters which need escaping, then that string can be
0870: * used as the string representation of the value.
0871: *
0872: * o a space or "#" character occurring at the beginning of the
0873: * string
0874: * o a space character occurring at the end of the string
0875: *
0876: * o one of the characters ",", "+", """, "\", "<", ">" or ";"
0877: *
0878: * If a character to be escaped is one of the list shown above, then
0879: * it is prefixed by a backslash ('\' ASCII 92).
0880: *
0881: * Otherwise the character to be escaped is replaced by a backslash
0882: * and two hex digits, which form a single byte in the code of the
0883: * character.
0884: */
0885: final String escapees = ",+<>;\"\\";
0886: StringBuffer sbuffer = new StringBuffer();
0887: boolean previousWhite = false;
0888:
0889: for (int i = 0; i < valStr.length(); i++) {
0890: char c = valStr.charAt(i);
0891:
0892: if (DerValue.isPrintableStringChar(c)
0893: || escapees.indexOf(c) >= 0
0894: || (i == 0 && c == '#')) {
0895:
0896: // escape leading '#' and escapees
0897: if ((i == 0 && c == '#')
0898: || escapees.indexOf(c) >= 0) {
0899: sbuffer.append('\\');
0900: }
0901:
0902: // convert multiple whitespace to single whitespace
0903: if (!Character.isWhitespace(c)) {
0904: previousWhite = false;
0905: sbuffer.append(c);
0906: } else {
0907: if (previousWhite == false) {
0908: // add single whitespace
0909: previousWhite = true;
0910: sbuffer.append(c);
0911: } else {
0912: // ignore subsequent consecutive whitespace
0913: continue;
0914: }
0915: }
0916:
0917: } else if (debug != null && Debug.isOn("ava")) {
0918:
0919: // embed non-printable/non-escaped char
0920: // as escaped hex pairs for debugging
0921:
0922: previousWhite = false;
0923:
0924: byte valueBytes[] = null;
0925: try {
0926: valueBytes = Character.toString(c).getBytes(
0927: "UTF8");
0928: } catch (IOException ie) {
0929: throw new IllegalArgumentException(
0930: "DER Value conversion");
0931: }
0932: for (int j = 0; j < valueBytes.length; j++) {
0933: sbuffer.append('\\');
0934: sbuffer.append(Character.forDigit(
0935: 0xF & (valueBytes[j] >>> 4), 16));
0936: sbuffer.append(Character.forDigit(
0937: 0xF & (valueBytes[j]), 16));
0938: }
0939: } else {
0940:
0941: // append non-printable/non-escaped char
0942:
0943: previousWhite = false;
0944: sbuffer.append(c);
0945: }
0946: }
0947:
0948: // remove leading and trailing whitespace from value
0949: typeAndValue.append(sbuffer.toString().trim());
0950: }
0951:
0952: String canon = new String(typeAndValue);
0953: canon = canon.toUpperCase(Locale.US).toLowerCase(Locale.US);
0954: return Normalizer.normalize(canon, Normalizer.DECOMP_COMPAT, 0);
0955: }
0956:
0957: /*
0958: * Return true if DerValue can be represented as a String.
0959: */
0960: private static boolean isDerString(DerValue value, boolean canonical) {
0961: if (canonical) {
0962: switch (value.tag) {
0963: case DerValue.tag_PrintableString:
0964: case DerValue.tag_UTF8String:
0965: return true;
0966: default:
0967: return false;
0968: }
0969: } else {
0970: switch (value.tag) {
0971: case DerValue.tag_PrintableString:
0972: case DerValue.tag_T61String:
0973: case DerValue.tag_IA5String:
0974: case DerValue.tag_GeneralString:
0975: case DerValue.tag_BMPString:
0976: case DerValue.tag_UTF8String:
0977: return true;
0978: default:
0979: return false;
0980: }
0981: }
0982: }
0983:
0984: boolean hasRFC2253Keyword() {
0985: return AVAKeyword.hasKeyword(oid, RFC2253);
0986: }
0987:
0988: private String toKeywordValueString(String keyword) {
0989: /*
0990: * Construct the value with as little copying and garbage
0991: * production as practical. First the keyword (mandatory),
0992: * then the equals sign, finally the value.
0993: */
0994: StringBuffer retval = new StringBuffer(40);
0995:
0996: retval.append(keyword);
0997: retval.append("=");
0998:
0999: try {
1000: String valStr = value.getAsString();
1001:
1002: if (valStr == null) {
1003:
1004: // rfc1779 specifies that attribute values associated
1005: // with non-standard keyword attributes may be represented
1006: // using the hex format below. This will be used only
1007: // when the value is not a string type
1008:
1009: byte data[] = value.toByteArray();
1010:
1011: retval.append('#');
1012: for (int i = 0; i < data.length; i++) {
1013: retval.append(hexDigits
1014: .charAt((data[i] >> 4) & 0x0f));
1015: retval.append(hexDigits.charAt(data[i] & 0x0f));
1016: }
1017:
1018: } else {
1019:
1020: boolean quoteNeeded = false;
1021: StringBuffer sbuffer = new StringBuffer();
1022: boolean previousWhite = false;
1023:
1024: /*
1025: * Special characters (e.g. AVA list separators) cause strings
1026: * to need quoting, or at least escaping. So do leading or
1027: * trailing spaces, and multiple internal spaces.
1028: */
1029: for (int i = 0; i < valStr.length(); i++) {
1030: char c = valStr.charAt(i);
1031: if (DerValue.isPrintableStringChar(c)
1032: || specialChars.indexOf(c) >= 0) {
1033:
1034: // quote if leading whitespace or special chars
1035: if (!quoteNeeded
1036: && ((i == 0 && (c == ' ' || c == '\n')) || specialChars
1037: .indexOf(c) >= 0)) {
1038: quoteNeeded = true;
1039: }
1040:
1041: // quote if multiple internal whitespace
1042: if (!(c == ' ' || c == '\n')) {
1043: // escape '"' and '\'
1044: if (c == '"' || c == '\\') {
1045: sbuffer.append('\\');
1046: }
1047: previousWhite = false;
1048: } else {
1049: if (!quoteNeeded && previousWhite) {
1050: quoteNeeded = true;
1051: }
1052: previousWhite = true;
1053: }
1054:
1055: sbuffer.append(c);
1056:
1057: } else if (debug != null && Debug.isOn("ava")) {
1058:
1059: // embed non-printable/non-escaped char
1060: // as escaped hex pairs for debugging
1061:
1062: previousWhite = false;
1063:
1064: // embed escaped hex pairs
1065: byte[] valueBytes = Character.toString(c)
1066: .getBytes("UTF8");
1067: for (int j = 0; j < valueBytes.length; j++) {
1068: sbuffer.append('\\');
1069: char hexChar = Character.forDigit(
1070: 0xF & (valueBytes[j] >>> 4), 16);
1071: sbuffer.append(Character
1072: .toUpperCase(hexChar));
1073: hexChar = Character.forDigit(
1074: 0xF & (valueBytes[j]), 16);
1075: sbuffer.append(Character
1076: .toUpperCase(hexChar));
1077: }
1078: } else {
1079:
1080: // append non-printable/non-escaped char
1081:
1082: previousWhite = false;
1083: sbuffer.append(c);
1084: }
1085: }
1086:
1087: // quote if trailing whitespace
1088: if (sbuffer.length() > 0) {
1089: char trailChar = sbuffer
1090: .charAt(sbuffer.length() - 1);
1091: if (trailChar == ' ' || trailChar == '\n') {
1092: quoteNeeded = true;
1093: }
1094: }
1095:
1096: // Emit the string ... quote it if needed
1097: if (quoteNeeded) {
1098: retval.append("\"" + sbuffer.toString() + "\"");
1099: } else {
1100: retval.append(sbuffer.toString());
1101: }
1102: }
1103: } catch (IOException e) {
1104: throw new IllegalArgumentException("DER Value conversion");
1105: }
1106:
1107: return retval.toString();
1108: }
1109:
1110: }
1111:
1112: /**
1113: * Helper class that allows conversion from String to ObjectIdentifier and
1114: * vice versa according to RFC1779, RFC2253, and an augmented version of
1115: * those standards.
1116: */
1117: class AVAKeyword {
1118:
1119: private static final Map oidMap, keywordMap;
1120:
1121: private String keyword;
1122: private ObjectIdentifier oid;
1123: private boolean rfc1779Compliant, rfc2253Compliant;
1124:
1125: private AVAKeyword(String keyword, ObjectIdentifier oid,
1126: boolean rfc1779Compliant, boolean rfc2253Compliant) {
1127: this .keyword = keyword;
1128: this .oid = oid;
1129: this .rfc1779Compliant = rfc1779Compliant;
1130: this .rfc2253Compliant = rfc2253Compliant;
1131:
1132: // register it
1133: oidMap.put(oid, this );
1134: keywordMap.put(keyword, this );
1135: }
1136:
1137: private boolean isCompliant(int standard) {
1138: switch (standard) {
1139: case AVA.RFC1779:
1140: return rfc1779Compliant;
1141: case AVA.RFC2253:
1142: return rfc2253Compliant;
1143: case AVA.DEFAULT:
1144: return true;
1145: default:
1146: // should not occur, internal error
1147: throw new IllegalArgumentException("Invalid standard "
1148: + standard);
1149: }
1150: }
1151:
1152: /**
1153: * Get an object identifier representing the specified keyword (or
1154: * string encoded object identifier) in the given standard.
1155: *
1156: * @throws IOException If the keyword is not valid in the specified standard
1157: */
1158: static ObjectIdentifier getOID(String keyword, int standard)
1159: throws IOException {
1160: keyword = keyword.toUpperCase();
1161: if (standard == AVA.RFC2253) {
1162: if (keyword.startsWith(" ") || keyword.endsWith(" ")) {
1163: throw new IOException(
1164: "Invalid leading or trailing space "
1165: + "in keyword \"" + keyword + "\"");
1166: }
1167: } else {
1168: keyword = keyword.trim();
1169: }
1170:
1171: AVAKeyword ak = (AVAKeyword) keywordMap.get(keyword);
1172: if ((ak != null) && ak.isCompliant(standard)) {
1173: return ak.oid;
1174: }
1175:
1176: // no keyword found or not standard compliant, check if OID string
1177:
1178: // RFC1779 requires, DEFAULT allows OID. prefix
1179: if (standard == AVA.RFC1779) {
1180: if (keyword.startsWith("OID.") == false) {
1181: throw new IOException("Invalid RFC1779 keyword: "
1182: + keyword);
1183: }
1184: keyword = keyword.substring(4);
1185: } else if (standard == AVA.DEFAULT) {
1186: if (keyword.startsWith("OID.")) {
1187: keyword = keyword.substring(4);
1188: }
1189: }
1190: boolean number = false;
1191: if (keyword.length() != 0) {
1192: char ch = keyword.charAt(0);
1193: if ((ch >= '0') && (ch <= '9')) {
1194: number = true;
1195: }
1196: }
1197: if (number == false) {
1198: throw new IOException("Invalid keyword \"" + keyword + "\"");
1199: }
1200: return new ObjectIdentifier(keyword);
1201: }
1202:
1203: /**
1204: * Get a keyword for the given ObjectIdentifier according to standard.
1205: * If no keyword is available, the ObjectIdentifier is encoded as a
1206: * String.
1207: */
1208: static String getKeyword(ObjectIdentifier oid, int standard) {
1209: AVAKeyword ak = (AVAKeyword) oidMap.get(oid);
1210: if ((ak != null) && ak.isCompliant(standard)) {
1211: return ak.keyword;
1212: }
1213: // no compliant keyword, use OID
1214: String oidString = oid.toString();
1215: if (standard == AVA.RFC2253) {
1216: return oidString;
1217: } else {
1218: return "OID." + oidString;
1219: }
1220: }
1221:
1222: /**
1223: * Test if oid has an associated keyword in standard.
1224: */
1225: static boolean hasKeyword(ObjectIdentifier oid, int standard) {
1226: AVAKeyword ak = (AVAKeyword) oidMap.get(oid);
1227: if (ak == null) {
1228: return false;
1229: }
1230: return ak.isCompliant(standard);
1231: }
1232:
1233: static {
1234: oidMap = new HashMap();
1235: keywordMap = new HashMap();
1236:
1237: // NOTE if multiple keywords are available for one OID, order
1238: // is significant!! Preferred *LAST*.
1239: new AVAKeyword("CN", X500Name.commonName_oid, true, true);
1240: new AVAKeyword("C", X500Name.countryName_oid, true, true);
1241: new AVAKeyword("L", X500Name.localityName_oid, true, true);
1242: new AVAKeyword("S", X500Name.stateName_oid, false, false);
1243: new AVAKeyword("ST", X500Name.stateName_oid, true, true);
1244: new AVAKeyword("O", X500Name.orgName_oid, true, true);
1245: new AVAKeyword("OU", X500Name.orgUnitName_oid, true, true);
1246: new AVAKeyword("T", X500Name.title_oid, false, false);
1247: new AVAKeyword("IP", X500Name.ipAddress_oid, false, false);
1248: new AVAKeyword("STREET", X500Name.streetAddress_oid, true, true);
1249: new AVAKeyword("DC", X500Name.DOMAIN_COMPONENT_OID, false, true);
1250: new AVAKeyword("DNQUALIFIER", X500Name.DNQUALIFIER_OID, false,
1251: false);
1252: new AVAKeyword("DNQ", X500Name.DNQUALIFIER_OID, false, false);
1253: new AVAKeyword("SURNAME", X500Name.SURNAME_OID, false, false);
1254: new AVAKeyword("GIVENNAME", X500Name.GIVENNAME_OID, false,
1255: false);
1256: new AVAKeyword("INITIALS", X500Name.INITIALS_OID, false, false);
1257: new AVAKeyword("GENERATION", X500Name.GENERATIONQUALIFIER_OID,
1258: false, false);
1259: new AVAKeyword("EMAIL", PKCS9Attribute.EMAIL_ADDRESS_OID,
1260: false, false);
1261: new AVAKeyword("EMAILADDRESS",
1262: PKCS9Attribute.EMAIL_ADDRESS_OID, false, false);
1263: new AVAKeyword("UID", X500Name.userid_oid, false, true);
1264: new AVAKeyword("SERIALNUMBER", X500Name.SERIALNUMBER_OID,
1265: false, false);
1266: }
1267: }
|