1 /* 2 * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.security.x509; 27 28 import java.io.ByteArrayOutputStream; 29 import java.io.IOException; 30 import java.io.OutputStream; 31 import java.io.Reader; 32 import java.security.AccessController; 33 import java.text.Normalizer; 34 import java.util.*; 35 36 import static java.nio.charset.StandardCharsets.UTF_8; 37 38 import sun.security.action.GetBooleanAction; 39 import sun.security.util.*; 40 import sun.security.pkcs.PKCS9Attribute; 41 42 43 /** 44 * X.500 Attribute-Value-Assertion (AVA): an attribute, as identified by 45 * some attribute ID, has some particular value. Values are as a rule ASN.1 46 * printable strings. A conventional set of type IDs is recognized when 47 * parsing (and generating) RFC 1779, 2253 or 4514 syntax strings. 48 * 49 * <P>AVAs are components of X.500 relative names. Think of them as being 50 * individual fields of a database record. The attribute ID is how you 51 * identify the field, and the value is part of a particular record. 52 * <p> 53 * Note that instances of this class are immutable. 54 * 55 * @see X500Name 56 * @see RDN 57 * 58 * 59 * @author David Brownell 60 * @author Amit Kapoor 61 * @author Hemma Prafullchandra 62 */ 63 public class AVA implements DerEncoder { 64 65 private static final Debug debug = Debug.getInstance("x509", "\t[AVA]"); 66 // See CR 6391482: if enabled this flag preserves the old but incorrect 67 // PrintableString encoding for DomainComponent. It may need to be set to 68 // avoid breaking preexisting certificates generated with sun.security APIs. 69 private static final boolean PRESERVE_OLD_DC_ENCODING = GetBooleanAction 70 .privilegedGetProperty("com.sun.security.preserveOldDCEncoding"); 71 72 /** 73 * DEFAULT format allows both RFC1779 and RFC2253 syntax and 74 * additional keywords. 75 */ 76 static final int DEFAULT = 1; 77 /** 78 * RFC1779 specifies format according to RFC1779. 79 */ 80 static final int RFC1779 = 2; 81 /** 82 * RFC2253 specifies format according to RFC2253. 83 */ 84 static final int RFC2253 = 3; 85 86 // currently not private, accessed directly from RDN 87 final ObjectIdentifier oid; 88 final DerValue value; 89 90 /* 91 * If the value has any of these characters in it, it must be quoted. 92 * Backslash and quote characters must also be individually escaped. 93 * Leading and trailing spaces, also multiple internal spaces, also 94 * call for quoting the whole string. 95 */ 96 private static final String specialChars1779 = ",=\n+<>#;\\\""; 97 98 /* 99 * In RFC2253, if the value has any of these characters in it, it 100 * must be quoted by a preceding \. 101 */ 102 private static final String specialChars2253 = ",=+<>#;\\\""; 103 104 /* 105 * includes special chars from RFC1779 and RFC2253, as well as ' ' from 106 * RFC 4514. 107 */ 108 private static final String specialCharsDefault = ",=\n+<>#;\\\" "; 109 private static final String escapedDefault = ",+<>;\""; 110 111 public AVA(ObjectIdentifier type, DerValue val) { 112 if ((type == null) || (val == null)) { 113 throw new NullPointerException(); 114 } 115 oid = type; 116 value = val; 117 } 118 119 /** 120 * Parse an RFC 1779, 2253 or 4514 style AVA string: CN=fee fie foe fum 121 * or perhaps with quotes. Not all defined AVA tags are supported; 122 * of current note are X.400 related ones (PRMD, ADMD, etc). 123 * 124 * This terminates at unescaped AVA separators ("+") or RDN 125 * separators (",", ";"), and removes cosmetic whitespace at the end of 126 * values. 127 */ 128 AVA(Reader in) throws IOException { 129 this(in, DEFAULT); 130 } 131 132 /** 133 * Parse an RFC 1779, 2253 or 4514 style AVA string: CN=fee fie foe fum 134 * or perhaps with quotes. Additional keywords can be specified in the 135 * keyword/OID map. 136 * 137 * This terminates at unescaped AVA separators ("+") or RDN 138 * separators (",", ";"), and removes cosmetic whitespace at the end of 139 * values. 140 */ 141 AVA(Reader in, Map<String, String> keywordMap) throws IOException { 142 this(in, DEFAULT, keywordMap); 143 } 144 145 /** 146 * Parse an AVA string formatted according to format. 147 */ 148 AVA(Reader in, int format) throws IOException { 149 this(in, format, Collections.<String, String>emptyMap()); 150 } 151 152 /** 153 * Parse an AVA string formatted according to format. 154 * 155 * @param in Reader containing AVA String 156 * @param format parsing format 157 * @param keywordMap a Map where a keyword String maps to a corresponding 158 * OID String. Each AVA keyword will be mapped to the corresponding OID. 159 * If an entry does not exist, it will fallback to the builtin 160 * keyword/OID mapping. 161 * @throws IOException if the AVA String is not valid in the specified 162 * format or an OID String from the keywordMap is improperly formatted 163 */ 164 AVA(Reader in, int format, Map<String, String> keywordMap) 165 throws IOException { 166 // assume format is one of DEFAULT or RFC2253 167 168 StringBuilder temp = new StringBuilder(); 169 int c; 170 171 /* 172 * First get the keyword indicating the attribute's type, 173 * and map it to the appropriate OID. 174 */ 175 while (true) { 176 c = readChar(in, "Incorrect AVA format"); 177 if (c == '=') { 178 break; 179 } 180 temp.append((char)c); 181 } 182 183 oid = AVAKeyword.getOID(temp.toString(), format, keywordMap); 184 185 /* 186 * Now parse the value. "#hex", a quoted string, or a string 187 * terminated by "+", ",", ";". Whitespace before or after 188 * the value is stripped away unless format is RFC2253. 189 */ 190 temp.setLength(0); 191 if (format == RFC2253) { 192 // read next character 193 c = in.read(); 194 if (c == ' ') { 195 throw new IOException("Incorrect AVA RFC2253 format - " + 196 "leading space must be escaped"); 197 } 198 } else { 199 // read next character skipping whitespace 200 do { 201 c = in.read(); 202 } while ((c == ' ') || (c == '\n')); 203 } 204 if (c == -1) { 205 // empty value 206 value = new DerValue(""); 207 return; 208 } 209 210 if (c == '#') { 211 value = parseHexString(in, format); 212 } else if ((c == '"') && (format != RFC2253)) { 213 value = parseQuotedString(in, temp); 214 } else { 215 value = parseString(in, c, format, temp); 216 } 217 } 218 219 /** 220 * Get the ObjectIdentifier of this AVA. 221 */ 222 public ObjectIdentifier getObjectIdentifier() { 223 return oid; 224 } 225 226 /** 227 * Get the value of this AVA as a DerValue. 228 */ 229 public DerValue getDerValue() { 230 return value; 231 } 232 233 /** 234 * Get the value of this AVA as a String. 235 * 236 * @exception RuntimeException if we could not obtain the string form 237 * (should not occur) 238 */ 239 public String getValueString() { 240 try { 241 String s = value.getAsString(); 242 if (s == null) { 243 throw new RuntimeException("AVA string is null"); 244 } 245 return s; 246 } catch (IOException e) { 247 // should not occur 248 throw new RuntimeException("AVA error: " + e, e); 249 } 250 } 251 252 private static DerValue parseHexString 253 (Reader in, int format) throws IOException { 254 255 int c; 256 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 257 byte b = 0; 258 int cNdx = 0; 259 while (true) { 260 c = in.read(); 261 262 if (isTerminator(c, format)) { 263 break; 264 } 265 266 int cVal = Hex.decoder().fromHex(c); 267 268 if (cVal < 0) { 269 throw new IOException("AVA parse, invalid hex " + 270 "digit: "+ (char)c); 271 } 272 273 if ((cNdx % 2) == 1) { 274 b = (byte)((b * 16) + (byte)(cVal)); 275 baos.write(b); 276 } else { 277 b = (byte)(cVal); 278 } 279 cNdx++; 280 } 281 282 // throw exception if no hex digits 283 if (cNdx == 0) { 284 throw new IOException("AVA parse, zero hex digits"); 285 } 286 287 // throw exception if odd number of hex digits 288 if (cNdx % 2 == 1) { 289 throw new IOException("AVA parse, odd number of hex digits"); 290 } 291 292 return new DerValue(baos.toByteArray()); 293 } 294 295 private DerValue parseQuotedString 296 (Reader in, StringBuilder temp) throws IOException { 297 298 // RFC1779 specifies that an entire RDN may be enclosed in double 299 // quotes. In this case the syntax is any sequence of 300 // backslash-specialChar, backslash-backslash, 301 // backslash-doublequote, or character other than backslash or 302 // doublequote. 303 int c = readChar(in, "Quoted string did not end in quote"); 304 305 List<Byte> embeddedHex = new ArrayList<>(); 306 boolean isPrintableString = true; 307 while (c != '"') { 308 if (c == '\\') { 309 c = readChar(in, "Quoted string did not end in quote"); 310 311 // check for embedded hex pairs 312 Byte hexByte = null; 313 if ((hexByte = getEmbeddedHexPair(c, in)) != null) { 314 315 // always encode AVAs with embedded hex as UTF8 316 isPrintableString = false; 317 318 // append consecutive embedded hex 319 // as single string later 320 embeddedHex.add(hexByte); 321 c = in.read(); 322 continue; 323 } 324 325 if (specialChars1779.indexOf((char)c) < 0) { 326 throw new IOException 327 ("Invalid escaped character in AVA: " + 328 (char)c); 329 } 330 } 331 332 // add embedded hex bytes before next char 333 if (embeddedHex.size() > 0) { 334 String hexString = getEmbeddedHexString(embeddedHex); 335 temp.append(hexString); 336 embeddedHex.clear(); 337 } 338 339 // check for non-PrintableString chars 340 isPrintableString &= DerValue.isPrintableStringChar((char)c); 341 temp.append((char)c); 342 c = readChar(in, "Quoted string did not end in quote"); 343 } 344 345 // add trailing embedded hex bytes 346 if (embeddedHex.size() > 0) { 347 String hexString = getEmbeddedHexString(embeddedHex); 348 temp.append(hexString); 349 embeddedHex.clear(); 350 } 351 352 do { 353 c = in.read(); 354 } while ((c == '\n') || (c == ' ')); 355 if (c != -1) { 356 throw new IOException("AVA had characters other than " 357 + "whitespace after terminating quote"); 358 } 359 360 // encode as PrintableString unless value contains 361 // non-PrintableString chars 362 if (this.oid.equals(PKCS9Attribute.EMAIL_ADDRESS_OID) || 363 (this.oid.equals(X500Name.DOMAIN_COMPONENT_OID) && 364 PRESERVE_OLD_DC_ENCODING == false)) { 365 // EmailAddress and DomainComponent must be IA5String 366 return new DerValue(DerValue.tag_IA5String, 367 temp.toString().trim()); 368 } else if (isPrintableString) { 369 return new DerValue(temp.toString().trim()); 370 } else { 371 return new DerValue(DerValue.tag_UTF8String, 372 temp.toString().trim()); 373 } 374 } 375 376 private DerValue parseString 377 (Reader in, int c, int format, StringBuilder temp) throws IOException { 378 379 List<Byte> embeddedHex = new ArrayList<>(); 380 boolean isPrintableString = true; 381 boolean escape = false; 382 boolean leadingChar = true; 383 int spaceCount = 0; 384 do { 385 escape = false; 386 if (c == '\\') { 387 escape = true; 388 c = readChar(in, "Invalid trailing backslash"); 389 390 // check for embedded hex pairs 391 Byte hexByte = null; 392 if ((hexByte = getEmbeddedHexPair(c, in)) != null) { 393 394 // always encode AVAs with embedded hex as UTF8 395 isPrintableString = false; 396 397 // append consecutive embedded hex 398 // as single string later 399 embeddedHex.add(hexByte); 400 c = in.read(); 401 leadingChar = false; 402 continue; 403 } 404 405 // check if character was improperly escaped 406 if (format == DEFAULT && 407 specialCharsDefault.indexOf((char)c) == -1) { 408 throw new IOException 409 ("Invalid escaped character in AVA: '" + 410 (char)c + "'"); 411 } else if (format == RFC2253) { 412 if (c == ' ') { 413 // only leading/trailing space can be escaped 414 if (!leadingChar && !trailingSpace(in)) { 415 throw new IOException 416 ("Invalid escaped space character " + 417 "in AVA. Only a leading or trailing " + 418 "space character can be escaped."); 419 } 420 } else if (c == '#') { 421 // only leading '#' can be escaped 422 if (!leadingChar) { 423 throw new IOException 424 ("Invalid escaped '#' character in AVA. " + 425 "Only a leading '#' can be escaped."); 426 } 427 } else if (specialChars2253.indexOf((char)c) == -1) { 428 throw new IOException 429 ("Invalid escaped character in AVA: '" + 430 (char)c + "'"); 431 } 432 } 433 } else { 434 // check if character should have been escaped 435 if (format == RFC2253) { 436 if (specialChars2253.indexOf((char)c) != -1) { 437 throw new IOException 438 ("Character '" + (char)c + 439 "' in AVA appears without escape"); 440 } 441 } else if (escapedDefault.indexOf((char)c) != -1) { 442 throw new IOException 443 ("Character '" + (char)c + 444 "' in AVA appears without escape"); 445 } 446 } 447 448 // add embedded hex bytes before next char 449 if (embeddedHex.size() > 0) { 450 // add space(s) before embedded hex bytes 451 for (int i = 0; i < spaceCount; i++) { 452 temp.append(' '); 453 } 454 spaceCount = 0; 455 456 String hexString = getEmbeddedHexString(embeddedHex); 457 temp.append(hexString); 458 embeddedHex.clear(); 459 } 460 461 // check for non-PrintableString chars 462 isPrintableString &= DerValue.isPrintableStringChar((char)c); 463 if (c == ' ' && escape == false) { 464 // do not add non-escaped spaces yet 465 // (non-escaped trailing spaces are ignored) 466 spaceCount++; 467 } else { 468 // add space(s) 469 for (int i = 0; i < spaceCount; i++) { 470 temp.append(' '); 471 } 472 spaceCount = 0; 473 temp.append((char)c); 474 } 475 c = in.read(); 476 leadingChar = false; 477 } while (isTerminator(c, format) == false); 478 479 if (format == RFC2253 && spaceCount > 0) { 480 throw new IOException("Incorrect AVA RFC2253 format - " + 481 "trailing space must be escaped"); 482 } 483 484 // add trailing embedded hex bytes 485 if (embeddedHex.size() > 0) { 486 String hexString = getEmbeddedHexString(embeddedHex); 487 temp.append(hexString); 488 embeddedHex.clear(); 489 } 490 491 // encode as PrintableString unless value contains 492 // non-PrintableString chars 493 if (this.oid.equals(PKCS9Attribute.EMAIL_ADDRESS_OID) || 494 (this.oid.equals(X500Name.DOMAIN_COMPONENT_OID) && 495 PRESERVE_OLD_DC_ENCODING == false)) { 496 // EmailAddress and DomainComponent must be IA5String 497 return new DerValue(DerValue.tag_IA5String, temp.toString()); 498 } else if (isPrintableString) { 499 return new DerValue(temp.toString()); 500 } else { 501 return new DerValue(DerValue.tag_UTF8String, temp.toString()); 502 } 503 } 504 505 private static Byte getEmbeddedHexPair(int c1, Reader in) 506 throws IOException { 507 Hex.Decoder dec = Hex.decoder(); 508 int hi = dec.fromHex(c1); 509 if (hi >= 0) { 510 int c2 = readChar(in, "unexpected EOF - " + 511 "escaped hex value must include two valid digits"); 512 513 int lo = dec.fromHex(c2); 514 if (lo >= 0) { 515 return (byte)((hi<<4) + lo); 516 } else { 517 throw new IOException 518 ("escaped hex value must include two valid digits"); 519 } 520 } 521 return null; 522 } 523 524 private static String getEmbeddedHexString(List<Byte> hexList) { 525 int n = hexList.size(); 526 byte[] hexBytes = new byte[n]; 527 for (int i = 0; i < n; i++) { 528 hexBytes[i] = hexList.get(i).byteValue(); 529 } 530 return new String(hexBytes, UTF_8); 531 } 532 533 private static boolean isTerminator(int ch, int format) { 534 switch (ch) { 535 case -1: 536 case '+': 537 case ',': 538 return true; 539 case ';': 540 return format != RFC2253; 541 default: 542 return false; 543 } 544 } 545 546 private static int readChar(Reader in, String errMsg) throws IOException { 547 int c = in.read(); 548 if (c == -1) { 549 throw new IOException(errMsg); 550 } 551 return c; 552 } 553 554 private static boolean trailingSpace(Reader in) throws IOException { 555 556 boolean trailing = false; 557 558 if (!in.markSupported()) { 559 // oh well 560 return true; 561 } else { 562 // make readAheadLimit huge - 563 // in practice, AVA was passed a StringReader from X500Name, 564 // and StringReader ignores readAheadLimit anyways 565 in.mark(9999); 566 while (true) { 567 int nextChar = in.read(); 568 if (nextChar == -1) { 569 trailing = true; 570 break; 571 } else if (nextChar == ' ') { 572 continue; 573 } else if (nextChar == '\\') { 574 int followingChar = in.read(); 575 if (followingChar != ' ') { 576 trailing = false; 577 break; 578 } 579 } else { 580 trailing = false; 581 break; 582 } 583 } 584 585 in.reset(); 586 return trailing; 587 } 588 } 589 590 AVA(DerValue derval) throws IOException { 591 // Individual attribute value assertions are SEQUENCE of two values. 592 // That'd be a "struct" outside of ASN.1. 593 if (derval.tag != DerValue.tag_Sequence) { 594 throw new IOException("AVA not a sequence"); 595 } 596 oid = derval.data.getOID(); 597 value = derval.data.getDerValue(); 598 599 if (derval.data.available() != 0) { 600 throw new IOException("AVA, extra bytes = " 601 + derval.data.available()); 602 } 603 } 604 605 AVA(DerInputStream in) throws IOException { 606 this(in.getDerValue()); 607 } 608 609 public boolean equals(Object obj) { 610 if (this == obj) { 611 return true; 612 } 613 if (obj instanceof AVA == false) { 614 return false; 615 } 616 AVA other = (AVA)obj; 617 return this.toRFC2253CanonicalString().equals 618 (other.toRFC2253CanonicalString()); 619 } 620 621 /** 622 * Returns a hashcode for this AVA. 623 * 624 * @return a hashcode for this AVA. 625 */ 626 public int hashCode() { 627 return toRFC2253CanonicalString().hashCode(); 628 } 629 630 /* 631 * AVAs are encoded as a SEQUENCE of two elements. 632 */ 633 public void encode(DerOutputStream out) throws IOException { 634 derEncode(out); 635 } 636 637 /** 638 * DER encode this object onto an output stream. 639 * Implements the <code>DerEncoder</code> interface. 640 * 641 * @param out 642 * the output stream on which to write the DER encoding. 643 * 644 * @exception IOException on encoding error. 645 */ 646 public void derEncode(OutputStream out) throws IOException { 647 DerOutputStream tmp = new DerOutputStream(); 648 DerOutputStream tmp2 = new DerOutputStream(); 649 650 tmp.putOID(oid); 651 value.encode(tmp); 652 tmp2.write(DerValue.tag_Sequence, tmp); 653 out.write(tmp2.toByteArray()); 654 } 655 656 private String toKeyword(int format, Map<String, String> oidMap) { 657 return AVAKeyword.getKeyword(oid, format, oidMap); 658 } 659 660 /** 661 * Returns a printable form of this attribute, using RFC 1779 662 * syntax for individual attribute/value assertions. 663 */ 664 public String toString() { 665 return toKeywordValueString 666 (toKeyword(DEFAULT, Collections.<String, String>emptyMap())); 667 } 668 669 /** 670 * Returns a printable form of this attribute, using RFC 1779 671 * syntax for individual attribute/value assertions. It only 672 * emits standardised keywords. 673 */ 674 public String toRFC1779String() { 675 return toRFC1779String(Collections.<String, String>emptyMap()); 676 } 677 678 /** 679 * Returns a printable form of this attribute, using RFC 1779 680 * syntax for individual attribute/value assertions. It 681 * emits standardised keywords, as well as keywords contained in the 682 * OID/keyword map. 683 */ 684 public String toRFC1779String(Map<String, String> oidMap) { 685 return toKeywordValueString(toKeyword(RFC1779, oidMap)); 686 } 687 688 /** 689 * Returns a printable form of this attribute, using RFC 2253 690 * syntax for individual attribute/value assertions. It only 691 * emits standardised keywords. 692 */ 693 public String toRFC2253String() { 694 return toRFC2253String(Collections.<String, String>emptyMap()); 695 } 696 697 /** 698 * Returns a printable form of this attribute, using RFC 2253 699 * syntax for individual attribute/value assertions. It 700 * emits standardised keywords, as well as keywords contained in the 701 * OID/keyword map. 702 */ 703 public String toRFC2253String(Map<String, String> oidMap) { 704 Hex.Encoder enc = Hex.encoder(); 705 /* 706 * Section 2.3: The AttributeTypeAndValue is encoded as the string 707 * representation of the AttributeType, followed by an equals character 708 * ('=' ASCII 61), followed by the string representation of the 709 * AttributeValue. The encoding of the AttributeValue is given in 710 * section 2.4. 711 */ 712 StringBuilder typeAndValue = new StringBuilder(100); 713 typeAndValue.append(toKeyword(RFC2253, oidMap)); 714 typeAndValue.append('='); 715 716 /* 717 * Section 2.4: Converting an AttributeValue from ASN.1 to a String. 718 * If the AttributeValue is of a type which does not have a string 719 * representation defined for it, then it is simply encoded as an 720 * octothorpe character ('#' ASCII 35) followed by the hexadecimal 721 * representation of each of the bytes of the BER encoding of the X.500 722 * AttributeValue. This form SHOULD be used if the AttributeType is of 723 * the dotted-decimal form. 724 */ 725 if ((typeAndValue.charAt(0) >= '0' && typeAndValue.charAt(0) <= '9') || 726 !isDerString(value, false)) 727 { 728 byte[] data = null; 729 try { 730 data = value.toByteArray(); 731 } catch (IOException ie) { 732 throw new IllegalArgumentException("DER Value conversion"); 733 } 734 typeAndValue.append('#'); 735 enc.encode(typeAndValue, data); 736 } else { 737 /* 738 * 2.4 (cont): Otherwise, if the AttributeValue is of a type which 739 * has a string representation, the value is converted first to a 740 * UTF-8 string according to its syntax specification. 741 * 742 * NOTE: this implementation only emits DirectoryStrings of the 743 * types returned by isDerString(). 744 */ 745 String valStr = null; 746 try { 747 valStr = new String(value.getDataBytes(), UTF_8); 748 } catch (IOException ie) { 749 throw new IllegalArgumentException("DER Value conversion"); 750 } 751 752 /* 753 * 2.4 (cont): If the UTF-8 string does not have any of the 754 * following characters which need escaping, then that string can be 755 * used as the string representation of the value. 756 * 757 * o a space or "#" character occurring at the beginning of the 758 * string 759 * o a space character occurring at the end of the string 760 * o one of the characters ",", "+", """, "\", "<", ">" or ";" 761 * 762 * Implementations MAY escape other characters. 763 * 764 * NOTE: this implementation also recognizes "=" and "#" as 765 * characters which need escaping, and null which is escaped as 766 * '\00' (see RFC 4514). 767 * 768 * If a character to be escaped is one of the list shown above, then 769 * it is prefixed by a backslash ('\' ASCII 92). 770 * 771 * Otherwise the character to be escaped is replaced by a backslash 772 * and two hex digits, which form a single byte in the code of the 773 * character. 774 */ 775 final String escapees = ",=+<>#;\"\\"; 776 StringBuilder sbuffer = new StringBuilder(); 777 778 for (int i = 0; i < valStr.length(); i++) { 779 char c = valStr.charAt(i); 780 if (DerValue.isPrintableStringChar(c) || 781 escapees.indexOf(c) >= 0) { 782 783 // escape escapees 784 if (escapees.indexOf(c) >= 0) { 785 sbuffer.append('\\'); 786 } 787 788 // append printable/escaped char 789 sbuffer.append(c); 790 791 } else if (c == '\u0000') { 792 // escape null character 793 sbuffer.append("\\00"); 794 795 } else if (debug != null && Debug.isOn("ava")) { 796 797 // embed non-printable/non-escaped char 798 // as escaped hex pairs for debugging 799 byte[] valueBytes = Character.toString(c).getBytes(UTF_8); 800 for (int j = 0; j < valueBytes.length; j++) { 801 sbuffer.append('\\'); 802 enc.encodeHexPair(sbuffer, valueBytes[j]); 803 } 804 } else { 805 806 // append non-printable/non-escaped char 807 sbuffer.append(c); 808 } 809 } 810 811 char[] chars = sbuffer.toString().toCharArray(); 812 sbuffer = new StringBuilder(); 813 814 // Find leading and trailing whitespace. 815 int lead; // index of first char that is not leading whitespace 816 for (lead = 0; lead < chars.length; lead++) { 817 if (chars[lead] != ' ' && chars[lead] != '\r') { 818 break; 819 } 820 } 821 int trail; // index of last char that is not trailing whitespace 822 for (trail = chars.length - 1; trail >= 0; trail--) { 823 if (chars[trail] != ' ' && chars[trail] != '\r') { 824 break; 825 } 826 } 827 828 // escape leading and trailing whitespace 829 for (int i = 0; i < chars.length; i++) { 830 char c = chars[i]; 831 if (i < lead || i > trail) { 832 sbuffer.append('\\'); 833 } 834 sbuffer.append(c); 835 } 836 typeAndValue.append(sbuffer); 837 } 838 return typeAndValue.toString(); 839 } 840 841 public String toRFC2253CanonicalString() { 842 Hex.Encoder enc = Hex.encoder(); 843 /* 844 * Section 2.3: The AttributeTypeAndValue is encoded as the string 845 * representation of the AttributeType, followed by an equals character 846 * ('=' ASCII 61), followed by the string representation of the 847 * AttributeValue. The encoding of the AttributeValue is given in 848 * section 2.4. 849 */ 850 StringBuilder typeAndValue = new StringBuilder(40); 851 typeAndValue.append 852 (toKeyword(RFC2253, Collections.<String, String>emptyMap())); 853 typeAndValue.append('='); 854 855 /* 856 * Section 2.4: Converting an AttributeValue from ASN.1 to a String. 857 * If the AttributeValue is of a type which does not have a string 858 * representation defined for it, then it is simply encoded as an 859 * octothorpe character ('#' ASCII 35) followed by the hexadecimal 860 * representation of each of the bytes of the BER encoding of the X.500 861 * AttributeValue. This form SHOULD be used if the AttributeType is of 862 * the dotted-decimal form. 863 */ 864 if ((typeAndValue.charAt(0) >= '0' && typeAndValue.charAt(0) <= '9') || 865 !isDerString(value, true)) 866 { 867 byte[] data = null; 868 try { 869 data = value.toByteArray(); 870 } catch (IOException ie) { 871 throw new IllegalArgumentException("DER Value conversion"); 872 } 873 typeAndValue.append('#'); 874 enc.encode(typeAndValue, data); 875 } else { 876 /* 877 * 2.4 (cont): Otherwise, if the AttributeValue is of a type which 878 * has a string representation, the value is converted first to a 879 * UTF-8 string according to its syntax specification. 880 * 881 * NOTE: this implementation only emits DirectoryStrings of the 882 * types returned by isDerString(). 883 */ 884 String valStr = null; 885 try { 886 valStr = new String(value.getDataBytes(), UTF_8); 887 } catch (IOException ie) { 888 throw new IllegalArgumentException("DER Value conversion"); 889 } 890 891 /* 892 * 2.4 (cont): If the UTF-8 string does not have any of the 893 * following characters which need escaping, then that string can be 894 * used as the string representation of the value. 895 * 896 * o a space or "#" character occurring at the beginning of the 897 * string 898 * o a space character occurring at the end of the string 899 * 900 * o one of the characters ",", "+", """, "\", "<", ">" or ";" 901 * 902 * If a character to be escaped is one of the list shown above, then 903 * it is prefixed by a backslash ('\' ASCII 92). 904 * 905 * Otherwise the character to be escaped is replaced by a backslash 906 * and two hex digits, which form a single byte in the code of the 907 * character. 908 */ 909 final String escapees = ",+<>;\"\\"; 910 StringBuilder sbuffer = new StringBuilder(); 911 boolean previousWhite = false; 912 913 for (int i = 0; i < valStr.length(); i++) { 914 char c = valStr.charAt(i); 915 916 if (DerValue.isPrintableStringChar(c) || 917 escapees.indexOf(c) >= 0 || 918 (i == 0 && c == '#')) { 919 920 // escape leading '#' and escapees 921 if ((i == 0 && c == '#') || escapees.indexOf(c) >= 0) { 922 sbuffer.append('\\'); 923 } 924 925 // convert multiple whitespace to single whitespace 926 if (!Character.isWhitespace(c)) { 927 previousWhite = false; 928 sbuffer.append(c); 929 } else { 930 if (previousWhite == false) { 931 // add single whitespace 932 previousWhite = true; 933 sbuffer.append(c); 934 } else { 935 // ignore subsequent consecutive whitespace 936 continue; 937 } 938 } 939 940 } else if (debug != null && Debug.isOn("ava")) { 941 // embed non-printable/non-escaped char 942 // as escaped hex pairs for debugging 943 944 previousWhite = false; 945 946 byte[] valueBytes = Character.toString(c).getBytes(UTF_8); 947 for (int j = 0; j < valueBytes.length; j++) { 948 sbuffer.append('\\'); 949 enc.encodeHexPair(sbuffer, valueBytes[j]); 950 } 951 } else { 952 953 // append non-printable/non-escaped char 954 955 previousWhite = false; 956 sbuffer.append(c); 957 } 958 } 959 960 // remove leading and trailing whitespace from value 961 typeAndValue.append(sbuffer.toString().trim()); 962 } 963 964 String canon = typeAndValue.toString(); 965 canon = canon.toUpperCase(Locale.US).toLowerCase(Locale.US); 966 return Normalizer.normalize(canon, Normalizer.Form.NFKD); 967 } 968 969 /* 970 * Return true if DerValue can be represented as a String. 971 */ 972 private static boolean isDerString(DerValue value, boolean canonical) { 973 if (canonical) { 974 switch (value.tag) { 975 case DerValue.tag_PrintableString: 976 case DerValue.tag_UTF8String: 977 return true; 978 default: 979 return false; 980 } 981 } else { 982 switch (value.tag) { 983 case DerValue.tag_PrintableString: 984 case DerValue.tag_T61String: 985 case DerValue.tag_IA5String: 986 case DerValue.tag_GeneralString: 987 case DerValue.tag_BMPString: 988 case DerValue.tag_UTF8String: 989 return true; 990 default: 991 return false; 992 } 993 } 994 } 995 996 boolean hasRFC2253Keyword() { 997 return AVAKeyword.hasKeyword(oid, RFC2253); 998 } 999 1000 private String toKeywordValueString(String keyword) { 1001 /* 1002 * Construct the value with as little copying and garbage 1003 * production as practical. First the keyword (mandatory), 1004 * then the equals sign, finally the value. 1005 */ 1006 StringBuilder retval = new StringBuilder(40); 1007 Hex.Encoder enc = Hex.encoder(); 1008 retval.append(keyword); 1009 retval.append('='); 1010 1011 try { 1012 String valStr = value.getAsString(); 1013 1014 if (valStr == null) { 1015 1016 // RFC 1779 specifies that attribute values associated 1017 // with non-standard keyword attributes may be represented 1018 // using the hex format below. This will be used only 1019 // when the value is not a string type 1020 1021 byte[] data = value.toByteArray(); 1022 1023 retval.append('#'); 1024 enc.encode(retval, data); 1025 } else { 1026 1027 boolean quoteNeeded = false; 1028 StringBuilder sbuffer = new StringBuilder(); 1029 boolean previousWhite = false; 1030 final String escapees = ",+=\n<>#;\\\""; 1031 1032 /* 1033 * Special characters (e.g. AVA list separators) cause strings 1034 * to need quoting, or at least escaping. So do leading or 1035 * trailing spaces, and multiple internal spaces. 1036 */ 1037 int length = valStr.length(); 1038 boolean alreadyQuoted = 1039 (length > 1 && valStr.charAt(0) == '\"' 1040 && valStr.charAt(length - 1) == '\"'); 1041 1042 for (int i = 0; i < length; i++) { 1043 char c = valStr.charAt(i); 1044 if (alreadyQuoted && (i == 0 || i == length - 1)) { 1045 sbuffer.append(c); 1046 continue; 1047 } 1048 if (DerValue.isPrintableStringChar(c) || 1049 escapees.indexOf(c) >= 0) { 1050 1051 // quote if leading whitespace or special chars 1052 if (!quoteNeeded && 1053 ((i == 0 && (c == ' ' || c == '\n')) || 1054 escapees.indexOf(c) >= 0)) { 1055 quoteNeeded = true; 1056 } 1057 1058 // quote if multiple internal whitespace 1059 if (!(c == ' ' || c == '\n')) { 1060 // escape '"' and '\' 1061 if (c == '"' || c == '\\') { 1062 sbuffer.append('\\'); 1063 } 1064 previousWhite = false; 1065 } else { 1066 if (!quoteNeeded && previousWhite) { 1067 quoteNeeded = true; 1068 } 1069 previousWhite = true; 1070 } 1071 1072 sbuffer.append(c); 1073 1074 } else if (debug != null && Debug.isOn("ava")) { 1075 1076 // embed non-printable/non-escaped char 1077 // as escaped hex pairs for debugging 1078 1079 previousWhite = false; 1080 1081 // embed escaped hex pairs 1082 byte[] valueBytes = 1083 Character.toString(c).getBytes(UTF_8); 1084 for (int j = 0; j < valueBytes.length; j++) { 1085 sbuffer.append('\\'); 1086 enc.encodeHexPair(sbuffer, valueBytes[j]); 1087 } 1088 } else { 1089 1090 // append non-printable/non-escaped char 1091 1092 previousWhite = false; 1093 sbuffer.append(c); 1094 } 1095 } 1096 1097 // quote if trailing whitespace 1098 if (sbuffer.length() > 0) { 1099 char trailChar = sbuffer.charAt(sbuffer.length() - 1); 1100 if (trailChar == ' ' || trailChar == '\n') { 1101 quoteNeeded = true; 1102 } 1103 } 1104 1105 // Emit the string ... quote it if needed 1106 // if string is already quoted, don't re-quote 1107 if (!alreadyQuoted && quoteNeeded) { 1108 retval.append('\"') 1109 .append(sbuffer) 1110 .append('\"'); 1111 } else { 1112 retval.append(sbuffer); 1113 } 1114 } 1115 } catch (IOException e) { 1116 throw new IllegalArgumentException("DER Value conversion"); 1117 } 1118 1119 return retval.toString(); 1120 } 1121 1122 } 1123 1124 /** 1125 * Helper class that allows conversion from String to ObjectIdentifier and 1126 * vice versa according to RFC1779, RFC2253, and an augmented version of 1127 * those standards. 1128 */ 1129 class AVAKeyword { 1130 1131 private static final Map<ObjectIdentifier,AVAKeyword> oidMap; 1132 private static final Map<String,AVAKeyword> keywordMap; 1133 1134 private String keyword; 1135 private ObjectIdentifier oid; 1136 private boolean rfc1779Compliant, rfc2253Compliant; 1137 1138 private AVAKeyword(String keyword, ObjectIdentifier oid, 1139 boolean rfc1779Compliant, boolean rfc2253Compliant) { 1140 this.keyword = keyword; 1141 this.oid = oid; 1142 this.rfc1779Compliant = rfc1779Compliant; 1143 this.rfc2253Compliant = rfc2253Compliant; 1144 1145 // register it 1146 oidMap.put(oid, this); 1147 keywordMap.put(keyword, this); 1148 } 1149 1150 private boolean isCompliant(int standard) { 1151 switch (standard) { 1152 case AVA.RFC1779: 1153 return rfc1779Compliant; 1154 case AVA.RFC2253: 1155 return rfc2253Compliant; 1156 case AVA.DEFAULT: 1157 return true; 1158 default: 1159 // should not occur, internal error 1160 throw new IllegalArgumentException("Invalid standard " + standard); 1161 } 1162 } 1163 1164 /** 1165 * Get an object identifier representing the specified keyword (or 1166 * string encoded object identifier) in the given standard. 1167 * 1168 * @param keywordMap a Map where a keyword String maps to a corresponding 1169 * OID String. Each AVA keyword will be mapped to the corresponding OID. 1170 * If an entry does not exist, it will fallback to the builtin 1171 * keyword/OID mapping. 1172 * @throws IOException If the keyword is not valid in the specified standard 1173 * or the OID String to which a keyword maps to is improperly formatted. 1174 */ 1175 static ObjectIdentifier getOID 1176 (String keyword, int standard, Map<String, String> extraKeywordMap) 1177 throws IOException { 1178 1179 keyword = keyword.toUpperCase(Locale.ENGLISH); 1180 if (standard == AVA.RFC2253) { 1181 if (keyword.startsWith(" ") || keyword.endsWith(" ")) { 1182 throw new IOException("Invalid leading or trailing space " + 1183 "in keyword \"" + keyword + "\""); 1184 } 1185 } else { 1186 keyword = keyword.trim(); 1187 } 1188 1189 // check user-specified keyword map first, then fallback to built-in 1190 // map 1191 String oidString = extraKeywordMap.get(keyword); 1192 if (oidString == null) { 1193 AVAKeyword ak = keywordMap.get(keyword); 1194 if ((ak != null) && ak.isCompliant(standard)) { 1195 return ak.oid; 1196 } 1197 } else { 1198 return ObjectIdentifier.of(oidString); 1199 } 1200 1201 // no keyword found, check if OID string 1202 if (standard == AVA.DEFAULT && keyword.startsWith("OID.")) { 1203 keyword = keyword.substring(4); 1204 } 1205 1206 boolean number = false; 1207 if (!keyword.isEmpty()) { 1208 char ch = keyword.charAt(0); 1209 if ((ch >= '0') && (ch <= '9')) { 1210 number = true; 1211 } 1212 } 1213 if (number == false) { 1214 throw new IOException("Invalid keyword \"" + keyword + "\""); 1215 } 1216 return ObjectIdentifier.of(keyword); 1217 } 1218 1219 /** 1220 * Get a keyword for the given ObjectIdentifier according to standard. 1221 * If no keyword is available, the ObjectIdentifier is encoded as a 1222 * String. 1223 */ 1224 static String getKeyword(ObjectIdentifier oid, int standard) { 1225 return getKeyword 1226 (oid, standard, Collections.<String, String>emptyMap()); 1227 } 1228 1229 /** 1230 * Get a keyword for the given ObjectIdentifier according to standard. 1231 * Checks the extraOidMap for a keyword first, then falls back to the 1232 * builtin/default set. If no keyword is available, the ObjectIdentifier 1233 * is encoded as a String. 1234 */ 1235 static String getKeyword 1236 (ObjectIdentifier oid, int standard, Map<String, String> extraOidMap) { 1237 1238 // check extraOidMap first, then fallback to built-in map 1239 String oidString = oid.toString(); 1240 String keywordString = extraOidMap.get(oidString); 1241 if (keywordString == null) { 1242 AVAKeyword ak = oidMap.get(oid); 1243 if ((ak != null) && ak.isCompliant(standard)) { 1244 return ak.keyword; 1245 } 1246 } else { 1247 if (keywordString.isEmpty()) { 1248 throw new IllegalArgumentException("keyword cannot be empty"); 1249 } 1250 keywordString = keywordString.trim(); 1251 char c = keywordString.charAt(0); 1252 if (c < 65 || c > 122 || (c > 90 && c < 97)) { 1253 throw new IllegalArgumentException 1254 ("keyword does not start with letter"); 1255 } 1256 for (int i=1; i<keywordString.length(); i++) { 1257 c = keywordString.charAt(i); 1258 if ((c < 65 || c > 122 || (c > 90 && c < 97)) && 1259 (c < 48 || c > 57) && c != '_') { 1260 throw new IllegalArgumentException 1261 ("keyword character is not a letter, digit, or underscore"); 1262 } 1263 } 1264 return keywordString; 1265 } 1266 // no compliant keyword, use OID 1267 if (standard == AVA.RFC2253) { 1268 return oidString; 1269 } else { 1270 return "OID." + oidString; 1271 } 1272 } 1273 1274 /** 1275 * Test if oid has an associated keyword in standard. 1276 */ 1277 static boolean hasKeyword(ObjectIdentifier oid, int standard) { 1278 AVAKeyword ak = oidMap.get(oid); 1279 if (ak == null) { 1280 return false; 1281 } 1282 return ak.isCompliant(standard); 1283 } 1284 1285 static { 1286 oidMap = new HashMap<ObjectIdentifier,AVAKeyword>(); 1287 keywordMap = new HashMap<String,AVAKeyword>(); 1288 1289 // NOTE if multiple keywords are available for one OID, order 1290 // is significant!! Preferred *LAST*. 1291 new AVAKeyword("CN", X500Name.commonName_oid, true, true); 1292 new AVAKeyword("C", X500Name.countryName_oid, true, true); 1293 new AVAKeyword("L", X500Name.localityName_oid, true, true); 1294 new AVAKeyword("S", X500Name.stateName_oid, false, false); 1295 new AVAKeyword("ST", X500Name.stateName_oid, true, true); 1296 new AVAKeyword("O", X500Name.orgName_oid, true, true); 1297 new AVAKeyword("OU", X500Name.orgUnitName_oid, true, true); 1298 new AVAKeyword("T", X500Name.title_oid, false, false); 1299 new AVAKeyword("IP", X500Name.ipAddress_oid, false, false); 1300 new AVAKeyword("STREET", X500Name.streetAddress_oid,true, true); 1301 new AVAKeyword("DC", X500Name.DOMAIN_COMPONENT_OID, 1302 false, true); 1303 new AVAKeyword("DNQUALIFIER", X500Name.DNQUALIFIER_OID, false, false); 1304 new AVAKeyword("DNQ", X500Name.DNQUALIFIER_OID, false, false); 1305 new AVAKeyword("SURNAME", X500Name.SURNAME_OID, false, false); 1306 new AVAKeyword("GIVENNAME", X500Name.GIVENNAME_OID, false, false); 1307 new AVAKeyword("INITIALS", X500Name.INITIALS_OID, false, false); 1308 new AVAKeyword("GENERATION", X500Name.GENERATIONQUALIFIER_OID, 1309 false, false); 1310 new AVAKeyword("EMAIL", PKCS9Attribute.EMAIL_ADDRESS_OID, false, false); 1311 new AVAKeyword("EMAILADDRESS", PKCS9Attribute.EMAIL_ADDRESS_OID, 1312 false, false); 1313 new AVAKeyword("UID", X500Name.userid_oid, false, true); 1314 new AVAKeyword("SERIALNUMBER", X500Name.SERIALNUMBER_OID, false, false); 1315 } 1316 }