1 /*
   2  * Copyright (c) 1996, 2018, 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.pkcs;
  27 
  28 import java.io.OutputStream;
  29 import java.io.IOException;
  30 import java.math.BigInteger;
  31 import java.security.cert.CertPathValidatorException;
  32 import java.security.cert.CertificateException;
  33 import java.security.cert.CertificateFactory;
  34 import java.security.cert.CertPath;
  35 import java.security.cert.X509Certificate;
  36 import java.security.*;
  37 import java.util.ArrayList;
  38 import java.util.Arrays;
  39 import java.util.Collections;
  40 import java.util.EnumSet;
  41 import java.util.Set;
  42 
  43 import sun.security.timestamp.TimestampToken;
  44 import sun.security.util.ConstraintsParameters;
  45 import sun.security.util.Debug;
  46 import sun.security.util.DerEncoder;
  47 import sun.security.util.DerInputStream;
  48 import sun.security.util.DerOutputStream;
  49 import sun.security.util.DerValue;
  50 import sun.security.util.DisabledAlgorithmConstraints;
  51 import sun.security.util.HexDumpEncoder;
  52 import sun.security.util.KeyUtil;
  53 import sun.security.util.ObjectIdentifier;
  54 import sun.security.x509.AlgorithmId;
  55 import sun.security.x509.X500Name;
  56 import sun.security.x509.KeyUsageExtension;
  57 import sun.security.util.SignatureUtil;
  58 
  59 /**
  60  * A SignerInfo, as defined in PKCS#7's signedData type.
  61  *
  62  * @author Benjamin Renaud
  63  */
  64 public class SignerInfo implements DerEncoder {
  65 
  66     // Digest and Signature restrictions
  67     private static final Set<CryptoPrimitive> DIGEST_PRIMITIVE_SET =
  68             Collections.unmodifiableSet(EnumSet.of(CryptoPrimitive.MESSAGE_DIGEST));
  69 
  70     private static final Set<CryptoPrimitive> SIG_PRIMITIVE_SET =
  71             Collections.unmodifiableSet(EnumSet.of(CryptoPrimitive.SIGNATURE));
  72 
  73     private static final DisabledAlgorithmConstraints JAR_DISABLED_CHECK =
  74             new DisabledAlgorithmConstraints(
  75                     DisabledAlgorithmConstraints.PROPERTY_JAR_DISABLED_ALGS);
  76 
  77     BigInteger version;
  78     X500Name issuerName;
  79     BigInteger certificateSerialNumber;
  80     AlgorithmId digestAlgorithmId;
  81     AlgorithmId digestEncryptionAlgorithmId;
  82     byte[] encryptedDigest;
  83     Timestamp timestamp;
  84     private boolean hasTimestamp = true;
  85     private static final Debug debug = Debug.getInstance("jar");
  86 
  87     PKCS9Attributes authenticatedAttributes;
  88     PKCS9Attributes unauthenticatedAttributes;
  89 
  90     public SignerInfo(X500Name  issuerName,
  91                       BigInteger serial,
  92                       AlgorithmId digestAlgorithmId,
  93                       AlgorithmId digestEncryptionAlgorithmId,
  94                       byte[] encryptedDigest) {
  95         this.version = BigInteger.ONE;
  96         this.issuerName = issuerName;
  97         this.certificateSerialNumber = serial;
  98         this.digestAlgorithmId = digestAlgorithmId;
  99         this.digestEncryptionAlgorithmId = digestEncryptionAlgorithmId;
 100         this.encryptedDigest = encryptedDigest;
 101     }
 102 
 103     public SignerInfo(X500Name  issuerName,
 104                       BigInteger serial,
 105                       AlgorithmId digestAlgorithmId,
 106                       PKCS9Attributes authenticatedAttributes,
 107                       AlgorithmId digestEncryptionAlgorithmId,
 108                       byte[] encryptedDigest,
 109                       PKCS9Attributes unauthenticatedAttributes) {
 110         this.version = BigInteger.ONE;
 111         this.issuerName = issuerName;
 112         this.certificateSerialNumber = serial;
 113         this.digestAlgorithmId = digestAlgorithmId;
 114         this.authenticatedAttributes = authenticatedAttributes;
 115         this.digestEncryptionAlgorithmId = digestEncryptionAlgorithmId;
 116         this.encryptedDigest = encryptedDigest;
 117         this.unauthenticatedAttributes = unauthenticatedAttributes;
 118     }
 119 
 120     /**
 121      * Parses a PKCS#7 signer info.
 122      */
 123     public SignerInfo(DerInputStream derin)
 124         throws IOException, ParsingException
 125     {
 126         this(derin, false);
 127     }
 128 
 129     /**
 130      * Parses a PKCS#7 signer info.
 131      *
 132      * <p>This constructor is used only for backwards compatibility with
 133      * PKCS#7 blocks that were generated using JDK1.1.x.
 134      *
 135      * @param derin the ASN.1 encoding of the signer info.
 136      * @param oldStyle flag indicating whether or not the given signer info
 137      * is encoded according to JDK1.1.x.
 138      */
 139     public SignerInfo(DerInputStream derin, boolean oldStyle)
 140         throws IOException, ParsingException
 141     {
 142         // version
 143         version = derin.getBigInteger();
 144 
 145         // issuerAndSerialNumber
 146         DerValue[] issuerAndSerialNumber = derin.getSequence(2);
 147         byte[] issuerBytes = issuerAndSerialNumber[0].toByteArray();
 148         issuerName = new X500Name(new DerValue(DerValue.tag_Sequence,
 149                                                issuerBytes));
 150         certificateSerialNumber = issuerAndSerialNumber[1].getBigInteger();
 151 
 152         // digestAlgorithmId
 153         DerValue tmp = derin.getDerValue();
 154 
 155         digestAlgorithmId = AlgorithmId.parse(tmp);
 156 
 157         // authenticatedAttributes
 158         if (oldStyle) {
 159             // In JDK1.1.x, the authenticatedAttributes are always present,
 160             // encoded as an empty Set (Set of length zero)
 161             derin.getSet(0);
 162         } else {
 163             // check if set of auth attributes (implicit tag) is provided
 164             // (auth attributes are OPTIONAL)
 165             if ((byte)(derin.peekByte()) == (byte)0xA0) {
 166                 authenticatedAttributes = new PKCS9Attributes(derin);
 167             }
 168         }
 169 
 170         // digestEncryptionAlgorithmId - little RSA naming scheme -
 171         // signature == encryption...
 172         tmp = derin.getDerValue();
 173 
 174         digestEncryptionAlgorithmId = AlgorithmId.parse(tmp);
 175 
 176         // encryptedDigest
 177         encryptedDigest = derin.getOctetString();
 178 
 179         // unauthenticatedAttributes
 180         if (oldStyle) {
 181             // In JDK1.1.x, the unauthenticatedAttributes are always present,
 182             // encoded as an empty Set (Set of length zero)
 183             derin.getSet(0);
 184         } else {
 185             // check if set of unauth attributes (implicit tag) is provided
 186             // (unauth attributes are OPTIONAL)
 187             if (derin.available() != 0
 188                 && (byte)(derin.peekByte()) == (byte)0xA1) {
 189                 unauthenticatedAttributes =
 190                     new PKCS9Attributes(derin, true);// ignore unsupported attrs
 191             }
 192         }
 193 
 194         // all done
 195         if (derin.available() != 0) {
 196             throw new ParsingException("extra data at the end");
 197         }
 198     }
 199 
 200     public void encode(DerOutputStream out) throws IOException {
 201 
 202         derEncode(out);
 203     }
 204 
 205     /**
 206      * DER encode this object onto an output stream.
 207      * Implements the {@code DerEncoder} interface.
 208      *
 209      * @param out
 210      * the output stream on which to write the DER encoding.
 211      *
 212      * @exception IOException on encoding error.
 213      */
 214     public void derEncode(OutputStream out) throws IOException {
 215         DerOutputStream seq = new DerOutputStream();
 216         seq.putInteger(version);
 217         DerOutputStream issuerAndSerialNumber = new DerOutputStream();
 218         issuerName.encode(issuerAndSerialNumber);
 219         issuerAndSerialNumber.putInteger(certificateSerialNumber);
 220         seq.write(DerValue.tag_Sequence, issuerAndSerialNumber);
 221 
 222         digestAlgorithmId.encode(seq);
 223 
 224         // encode authenticated attributes if there are any
 225         if (authenticatedAttributes != null)
 226             authenticatedAttributes.encode((byte)0xA0, seq);
 227 
 228         digestEncryptionAlgorithmId.encode(seq);
 229 
 230         seq.putOctetString(encryptedDigest);
 231 
 232         // encode unauthenticated attributes if there are any
 233         if (unauthenticatedAttributes != null)
 234             unauthenticatedAttributes.encode((byte)0xA1, seq);
 235 
 236         DerOutputStream tmp = new DerOutputStream();
 237         tmp.write(DerValue.tag_Sequence, seq);
 238 
 239         out.write(tmp.toByteArray());
 240     }
 241 
 242 
 243 
 244     /*
 245      * Returns the (user) certificate pertaining to this SignerInfo.
 246      */
 247     public X509Certificate getCertificate(PKCS7 block)
 248         throws IOException
 249     {
 250         return block.getCertificate(certificateSerialNumber, issuerName);
 251     }
 252 
 253     /*
 254      * Returns the certificate chain pertaining to this SignerInfo.
 255      */
 256     public ArrayList<X509Certificate> getCertificateChain(PKCS7 block)
 257         throws IOException
 258     {
 259         X509Certificate userCert;
 260         userCert = block.getCertificate(certificateSerialNumber, issuerName);
 261         if (userCert == null)
 262             return null;
 263 
 264         ArrayList<X509Certificate> certList = new ArrayList<>();
 265         certList.add(userCert);
 266 
 267         X509Certificate[] pkcsCerts = block.getCertificates();
 268         if (pkcsCerts == null
 269             || userCert.getSubjectDN().equals(userCert.getIssuerDN())) {
 270             return certList;
 271         }
 272 
 273         Principal issuer = userCert.getIssuerDN();
 274         int start = 0;
 275         while (true) {
 276             boolean match = false;
 277             int i = start;
 278             while (i < pkcsCerts.length) {
 279                 if (issuer.equals(pkcsCerts[i].getSubjectDN())) {
 280                     // next cert in chain found
 281                     certList.add(pkcsCerts[i]);
 282                     // if selected cert is self-signed, we're done
 283                     // constructing the chain
 284                     if (pkcsCerts[i].getSubjectDN().equals(
 285                                             pkcsCerts[i].getIssuerDN())) {
 286                         start = pkcsCerts.length;
 287                     } else {
 288                         issuer = pkcsCerts[i].getIssuerDN();
 289                         X509Certificate tmpCert = pkcsCerts[start];
 290                         pkcsCerts[start] = pkcsCerts[i];
 291                         pkcsCerts[i] = tmpCert;
 292                         start++;
 293                     }
 294                     match = true;
 295                     break;
 296                 } else {
 297                     i++;
 298                 }
 299             }
 300             if (!match)
 301                 break;
 302         }
 303 
 304         return certList;
 305     }
 306 
 307     /* Returns null if verify fails, this signerInfo if
 308        verify succeeds. */
 309     SignerInfo verify(PKCS7 block, byte[] data)
 310     throws NoSuchAlgorithmException, SignatureException {
 311 
 312         try {
 313 
 314             ContentInfo content = block.getContentInfo();
 315             if (data == null) {
 316                 data = content.getContentBytes();
 317             }
 318 
 319             Timestamp timestamp = null;
 320             try {
 321                 timestamp = getTimestamp();
 322             } catch (Exception ignore) {
 323             }
 324 
 325             ConstraintsParameters cparams =
 326                     new ConstraintsParameters(timestamp);
 327             String digestAlgname = getDigestAlgorithmId().getName();
 328 
 329             byte[] dataSigned;
 330 
 331             // if there are authenticate attributes, get the message
 332             // digest and compare it with the digest of data
 333             if (authenticatedAttributes == null) {
 334                 dataSigned = data;
 335             } else {
 336 
 337                 // first, check content type
 338                 ObjectIdentifier contentType = (ObjectIdentifier)
 339                        authenticatedAttributes.getAttributeValue(
 340                          PKCS9Attribute.CONTENT_TYPE_OID);
 341                 if (contentType == null ||
 342                     !contentType.equals(content.contentType))
 343                     return null;  // contentType does not match, bad SignerInfo
 344 
 345                 // now, check message digest
 346                 byte[] messageDigest = (byte[])
 347                     authenticatedAttributes.getAttributeValue(
 348                          PKCS9Attribute.MESSAGE_DIGEST_OID);
 349 
 350                 if (messageDigest == null) // fail if there is no message digest
 351                     return null;
 352 
 353                 // check that digest algorithm is not restricted
 354                 try {
 355                     JAR_DISABLED_CHECK.permits(digestAlgname, cparams);
 356                 } catch (CertPathValidatorException e) {
 357                     throw new SignatureException(e.getMessage(), e);
 358                 }
 359 
 360                 MessageDigest md = MessageDigest.getInstance(digestAlgname);
 361                 byte[] computedMessageDigest = md.digest(data);
 362 
 363                 if (messageDigest.length != computedMessageDigest.length)
 364                     return null;
 365                 for (int i = 0; i < messageDigest.length; i++) {
 366                     if (messageDigest[i] != computedMessageDigest[i])
 367                         return null;
 368                 }
 369 
 370                 // message digest attribute matched
 371                 // digest of original data
 372 
 373                 // the data actually signed is the DER encoding of
 374                 // the authenticated attributes (tagged with
 375                 // the "SET OF" tag, not 0xA0).
 376                 dataSigned = authenticatedAttributes.getDerEncoding();
 377             }
 378 
 379             // put together digest algorithm and encryption algorithm
 380             // to form signing algorithm
 381             String encryptionAlgname =
 382                 getDigestEncryptionAlgorithmId().getName();
 383 
 384             // Workaround: sometimes the encryptionAlgname is actually
 385             // a signature name
 386             String tmp = AlgorithmId.getEncAlgFromSigAlg(encryptionAlgname);
 387             if (tmp != null) encryptionAlgname = tmp;
 388             String algname = AlgorithmId.makeSigAlg(
 389                     digestAlgname, encryptionAlgname);
 390 
 391             // check that jar signature algorithm is not restricted
 392             try {
 393                 JAR_DISABLED_CHECK.permits(algname, cparams);
 394             } catch (CertPathValidatorException e) {
 395                 throw new SignatureException(e.getMessage(), e);
 396             }
 397 
 398             X509Certificate cert = getCertificate(block);
 399             if (cert == null) {
 400                 return null;
 401             }
 402             PublicKey key = cert.getPublicKey();
 403 
 404             // check if the public key is restricted
 405             if (!JAR_DISABLED_CHECK.permits(SIG_PRIMITIVE_SET, key)) {
 406                 throw new SignatureException("Public key check failed. " +
 407                         "Disabled key used: " +
 408                         KeyUtil.getKeySize(key) + " bit " +
 409                         key.getAlgorithm());
 410             }
 411 
 412             if (cert.hasUnsupportedCriticalExtension()) {
 413                 throw new SignatureException("Certificate has unsupported "
 414                                              + "critical extension(s)");
 415             }
 416 
 417             // Make sure that if the usage of the key in the certificate is
 418             // restricted, it can be used for digital signatures.
 419             // XXX We may want to check for additional extensions in the
 420             // future.
 421             boolean[] keyUsageBits = cert.getKeyUsage();
 422             if (keyUsageBits != null) {
 423                 KeyUsageExtension keyUsage;
 424                 try {
 425                     // We don't care whether or not this extension was marked
 426                     // critical in the certificate.
 427                     // We're interested only in its value (i.e., the bits set)
 428                     // and treat the extension as critical.
 429                     keyUsage = new KeyUsageExtension(keyUsageBits);
 430                 } catch (IOException ioe) {
 431                     throw new SignatureException("Failed to parse keyUsage "
 432                                                  + "extension");
 433                 }
 434 
 435                 boolean digSigAllowed = keyUsage.get(
 436                         KeyUsageExtension.DIGITAL_SIGNATURE).booleanValue();
 437 
 438                 boolean nonRepuAllowed = keyUsage.get(
 439                         KeyUsageExtension.NON_REPUDIATION).booleanValue();
 440 
 441                 if (!digSigAllowed && !nonRepuAllowed) {
 442                     throw new SignatureException("Key usage restricted: "
 443                                                  + "cannot be used for "
 444                                                  + "digital signatures");
 445                 }
 446             }
 447 
 448             Signature sig = Signature.getInstance(algname);
 449 
 450             // set parameters before Signature.initSign/initVerify call,
 451             // so key can be checked when it's set
 452             AlgorithmParameters ap =
 453                 digestEncryptionAlgorithmId.getParameters();
 454             try {
 455                 SignatureUtil.specialSetParameter(sig, ap);
 456             } catch (ProviderException | InvalidAlgorithmParameterException e) {
 457                 throw new SignatureException(e.getMessage(), e);
 458             }
 459 
 460             sig.initVerify(key);
 461             sig.update(dataSigned);
 462             if (sig.verify(encryptedDigest)) {
 463                 return this;
 464             }
 465         } catch (IOException e) {
 466             throw new SignatureException("IO error verifying signature:\n" +
 467                                          e.getMessage());
 468         } catch (InvalidKeyException e) {
 469             throw new SignatureException("InvalidKey: " + e.getMessage());
 470         }
 471         return null;
 472     }
 473 
 474     /* Verify the content of the pkcs7 block. */
 475     SignerInfo verify(PKCS7 block)
 476         throws NoSuchAlgorithmException, SignatureException {
 477         return verify(block, null);
 478     }
 479 
 480     public BigInteger getVersion() {
 481             return version;
 482     }
 483 
 484     public X500Name getIssuerName() {
 485         return issuerName;
 486     }
 487 
 488     public BigInteger getCertificateSerialNumber() {
 489         return certificateSerialNumber;
 490     }
 491 
 492     public AlgorithmId getDigestAlgorithmId() {
 493         return digestAlgorithmId;
 494     }
 495 
 496     public PKCS9Attributes getAuthenticatedAttributes() {
 497         return authenticatedAttributes;
 498     }
 499 
 500     public AlgorithmId getDigestEncryptionAlgorithmId() {
 501         return digestEncryptionAlgorithmId;
 502     }
 503 
 504     public byte[] getEncryptedDigest() {
 505         return encryptedDigest;
 506     }
 507 
 508     public PKCS9Attributes getUnauthenticatedAttributes() {
 509         return unauthenticatedAttributes;
 510     }
 511 
 512     /**
 513      * Returns the timestamp PKCS7 data unverified.
 514      * @return a PKCS7 object
 515      */
 516     public PKCS7 getTsToken() throws IOException {
 517         if (unauthenticatedAttributes == null) {
 518             return null;
 519         }
 520         PKCS9Attribute tsTokenAttr =
 521                 unauthenticatedAttributes.getAttribute(
 522                         PKCS9Attribute.SIGNATURE_TIMESTAMP_TOKEN_OID);
 523         if (tsTokenAttr == null) {
 524             return null;
 525         }
 526         return new PKCS7((byte[])tsTokenAttr.getValue());
 527     }
 528 
 529     /*
 530      * Extracts a timestamp from a PKCS7 SignerInfo.
 531      *
 532      * Examines the signer's unsigned attributes for a
 533      * {@code signatureTimestampToken} attribute. If present,
 534      * then it is parsed to extract the date and time at which the
 535      * timestamp was generated.
 536      *
 537      * @param info A signer information element of a PKCS 7 block.
 538      *
 539      * @return A timestamp token or null if none is present.
 540      * @throws IOException if an error is encountered while parsing the
 541      *         PKCS7 data.
 542      * @throws NoSuchAlgorithmException if an error is encountered while
 543      *         verifying the PKCS7 object.
 544      * @throws SignatureException if an error is encountered while
 545      *         verifying the PKCS7 object.
 546      * @throws CertificateException if an error is encountered while generating
 547      *         the TSA's certpath.
 548      */
 549     public Timestamp getTimestamp()
 550         throws IOException, NoSuchAlgorithmException, SignatureException,
 551                CertificateException
 552     {
 553         if (timestamp != null || !hasTimestamp)
 554             return timestamp;
 555 
 556         PKCS7 tsToken = getTsToken();
 557         if (tsToken == null) {
 558             hasTimestamp = false;
 559             return null;
 560         }
 561 
 562         // Extract the content (an encoded timestamp token info)
 563         byte[] encTsTokenInfo = tsToken.getContentInfo().getData();
 564         // Extract the signer (the Timestamping Authority)
 565         // while verifying the content
 566         SignerInfo[] tsa = tsToken.verify(encTsTokenInfo);
 567         // Expect only one signer
 568         ArrayList<X509Certificate> chain = tsa[0].getCertificateChain(tsToken);
 569         CertificateFactory cf = CertificateFactory.getInstance("X.509");
 570         CertPath tsaChain = cf.generateCertPath(chain);
 571         // Create a timestamp token info object
 572         TimestampToken tsTokenInfo = new TimestampToken(encTsTokenInfo);
 573         // Check that the signature timestamp applies to this signature
 574         verifyTimestamp(tsTokenInfo);
 575         // Create a timestamp object
 576         timestamp = new Timestamp(tsTokenInfo.getDate(), tsaChain);
 577         return timestamp;
 578     }
 579 
 580     /*
 581      * Check that the signature timestamp applies to this signature.
 582      * Match the hash present in the signature timestamp token against the hash
 583      * of this signature.
 584      */
 585     private void verifyTimestamp(TimestampToken token)
 586         throws NoSuchAlgorithmException, SignatureException {
 587         String digestAlgname = token.getHashAlgorithm().getName();
 588         // check that algorithm is not restricted
 589         if (!JAR_DISABLED_CHECK.permits(DIGEST_PRIMITIVE_SET, digestAlgname,
 590                 null)) {
 591             throw new SignatureException("Timestamp token digest check failed. " +
 592                     "Disabled algorithm used: " + digestAlgname);
 593         }
 594 
 595         MessageDigest md =
 596             MessageDigest.getInstance(digestAlgname);
 597 
 598         if (!Arrays.equals(token.getHashedMessage(),
 599             md.digest(encryptedDigest))) {
 600 
 601             throw new SignatureException("Signature timestamp (#" +
 602                 token.getSerialNumber() + ") generated on " + token.getDate() +
 603                 " is inapplicable");
 604         }
 605 
 606         if (debug != null) {
 607             debug.println();
 608             debug.println("Detected signature timestamp (#" +
 609                 token.getSerialNumber() + ") generated on " + token.getDate());
 610             debug.println();
 611         }
 612     }
 613 
 614     public String toString() {
 615         HexDumpEncoder hexDump = new HexDumpEncoder();
 616 
 617         String out = "";
 618 
 619         out += "Signer Info for (issuer): " + issuerName + "\n";
 620         out += "\tversion: " + Debug.toHexString(version) + "\n";
 621         out += "\tcertificateSerialNumber: " +
 622                Debug.toHexString(certificateSerialNumber) + "\n";
 623         out += "\tdigestAlgorithmId: " + digestAlgorithmId + "\n";
 624         if (authenticatedAttributes != null) {
 625             out += "\tauthenticatedAttributes: " + authenticatedAttributes +
 626                    "\n";
 627         }
 628         out += "\tdigestEncryptionAlgorithmId: " + digestEncryptionAlgorithmId +
 629             "\n";
 630 
 631         out += "\tencryptedDigest: " + "\n" +
 632             hexDump.encodeBuffer(encryptedDigest) + "\n";
 633         if (unauthenticatedAttributes != null) {
 634             out += "\tunauthenticatedAttributes: " +
 635                    unauthenticatedAttributes + "\n";
 636         }
 637         return out;
 638     }
 639 }