History

My first initial commit with all of mine experimental changes.

Dmitry Kabak (userdima committed 13 years ago

Commit d37b88d1 parent 3f5b46618352 files · +2598093 −-8690Browse files

Showing the changes to java/pkg/sun/security/provider/certpath/OCSPResponse.java (1 of 18352 files). Show all

Unified · Split

java/pkg/sun/security/provider/certpath/OCSPResponse.java+676−0
@@ -0,0 +1,676 @@
1/*
2 * Copyright (c) 2003, 2010, 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
26package sun.security.provider.certpath;
27
28import java.io.*;
29import java.math.BigInteger;
30import java.security.*;
31import java.security.cert.CertificateException;
32import java.security.cert.CertificateParsingException;
33import java.security.cert.CertPathValidatorException;
34import java.security.cert.CRLReason;
35import java.security.cert.TrustAnchor;
36import java.security.cert.X509Certificate;
37import java.util.Collections;
38import java.util.Date;
39import java.util.HashMap;
40import java.util.List;
41import java.util.Map;
42import sun.misc.HexDumpEncoder;
43import sun.security.x509.*;
44import sun.security.util.*;
45
46/**
47 * This class is used to process an OCSP response.
48 * The OCSP Response is defined
49 * in RFC 2560 and the ASN.1 encoding is as follows:
50 * <pre>
51 *
52 * OCSPResponse ::= SEQUENCE {
53 * responseStatus OCSPResponseStatus,
54 * responseBytes [0] EXPLICIT ResponseBytes OPTIONAL }
55 *
56 * OCSPResponseStatus ::= ENUMERATED {
57 * successful (0), --Response has valid confirmations
58 * malformedRequest (1), --Illegal confirmation request
59 * internalError (2), --Internal error in issuer
60 * tryLater (3), --Try again later
61 * --(4) is not used
62 * sigRequired (5), --Must sign the request
63 * unauthorized (6) --Request unauthorized
64 * }
65 *
66 * ResponseBytes ::= SEQUENCE {
67 * responseType OBJECT IDENTIFIER,
68 * response OCTET STRING }
69 *
70 * BasicOCSPResponse ::= SEQUENCE {
71 * tbsResponseData ResponseData,
72 * signatureAlgorithm AlgorithmIdentifier,
73 * signature BIT STRING,
74 * certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
75 *
76 * The value for signature SHALL be computed on the hash of the DER
77 * encoding ResponseData.
78 *
79 * ResponseData ::= SEQUENCE {
80 * version [0] EXPLICIT Version DEFAULT v1,
81 * responderID ResponderID,
82 * producedAt GeneralizedTime,
83 * responses SEQUENCE OF SingleResponse,
84 * responseExtensions [1] EXPLICIT Extensions OPTIONAL }
85 *
86 * ResponderID ::= CHOICE {
87 * byName [1] Name,
88 * byKey [2] KeyHash }
89 *
90 * KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
91 * (excluding the tag and length fields)
92 *
93 * SingleResponse ::= SEQUENCE {
94 * certID CertID,
95 * certStatus CertStatus,
96 * thisUpdate GeneralizedTime,
97 * nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL,
98 * singleExtensions [1] EXPLICIT Extensions OPTIONAL }
99 *
100 * CertStatus ::= CHOICE {
101 * good [0] IMPLICIT NULL,
102 * revoked [1] IMPLICIT RevokedInfo,
103 * unknown [2] IMPLICIT UnknownInfo }
104 *
105 * RevokedInfo ::= SEQUENCE {
106 * revocationTime GeneralizedTime,
107 * revocationReason [0] EXPLICIT CRLReason OPTIONAL }
108 *
109 * UnknownInfo ::= NULL -- this can be replaced with an enumeration
110 *
111 * </pre>
112 *
113 * @author Ram Marti
114 */
115
116public final class OCSPResponse {
117
118 public enum ResponseStatus {
119 SUCCESSFUL, // Response has valid confirmations
120 MALFORMED_REQUEST, // Illegal confirmation request
121 INTERNAL_ERROR, // Internal error in issuer
122 TRY_LATER, // Try again later
123 UNUSED, // is not used
124 SIG_REQUIRED, // Must sign the request
125 UNAUTHORIZED // Request unauthorized
126 };
127 private static ResponseStatus[] rsvalues = ResponseStatus.values();
128
129 private static final Debug DEBUG = Debug.getInstance("certpath");
130 private static final boolean dump = false;
131 private static final ObjectIdentifier OCSP_BASIC_RESPONSE_OID =
132 ObjectIdentifier.newInternal(new int[] { 1, 3, 6, 1, 5, 5, 7, 48, 1, 1});
133 private static final ObjectIdentifier OCSP_NONCE_EXTENSION_OID =
134 ObjectIdentifier.newInternal(new int[] { 1, 3, 6, 1, 5, 5, 7, 48, 1, 2});
135
136 private static final int CERT_STATUS_GOOD = 0;
137 private static final int CERT_STATUS_REVOKED = 1;
138 private static final int CERT_STATUS_UNKNOWN = 2;
139
140 // ResponderID CHOICE tags
141 private static final int NAME_TAG = 1;
142 private static final int KEY_TAG = 2;
143
144 // Object identifier for the OCSPSigning key purpose
145 private static final String KP_OCSP_SIGNING_OID = "1.3.6.1.5.5.7.3.9";
146
147 private final ResponseStatus responseStatus;
148 private final Map<CertId, SingleResponse> singleResponseMap;
149
150 // Maximum clock skew in milliseconds (15 minutes) allowed when checking
151 // validity of OCSP responses
152 private static final long MAX_CLOCK_SKEW = 900000;
153
154 // an array of all of the CRLReasons (used in SingleResponse)
155 private static CRLReason[] values = CRLReason.values();
156
157 /*
158 * Create an OCSP response from its ASN.1 DER encoding.
159 */
160 OCSPResponse(byte[] bytes, Date dateCheckedAgainst,
161 X509Certificate responderCert)
162 throws IOException, CertPathValidatorException {
163
164 // OCSPResponse
165 if (dump) {
166 HexDumpEncoder hexEnc = new HexDumpEncoder();
167 System.out.println("OCSPResponse bytes are...");
168 System.out.println(hexEnc.encode(bytes));
169 }
170 DerValue der = new DerValue(bytes);
171 if (der.tag != DerValue.tag_Sequence) {
172 throw new IOException("Bad encoding in OCSP response: " +
173 "expected ASN.1 SEQUENCE tag.");
174 }
175 DerInputStream derIn = der.getData();
176
177 // responseStatus
178 int status = derIn.getEnumerated();
179 if (status >= 0 && status < rsvalues.length) {
180 responseStatus = rsvalues[status];
181 } else {
182 // unspecified responseStatus
183 throw new IOException("Unknown OCSPResponse status: " + status);
184 }
185 if (DEBUG != null) {
186 DEBUG.println("OCSP response status: " + responseStatus);
187 }
188 if (responseStatus != ResponseStatus.SUCCESSFUL) {
189 // no need to continue, responseBytes are not set.
190 singleResponseMap = Collections.emptyMap();
191 return;
192 }
193
194 // responseBytes
195 der = derIn.getDerValue();
196 if (!der.isContextSpecific((byte)0)) {
197 throw new IOException("Bad encoding in responseBytes element " +
198 "of OCSP response: expected ASN.1 context specific tag 0.");
199 }
200 DerValue tmp = der.data.getDerValue();
201 if (tmp.tag != DerValue.tag_Sequence) {
202 throw new IOException("Bad encoding in responseBytes element " +
203 "of OCSP response: expected ASN.1 SEQUENCE tag.");
204 }
205
206 // responseType
207 derIn = tmp.data;
208 ObjectIdentifier responseType = derIn.getOID();
209 if (responseType.equals(OCSP_BASIC_RESPONSE_OID)) {
210 if (DEBUG != null) {
211 DEBUG.println("OCSP response type: basic");
212 }
213 } else {
214 if (DEBUG != null) {
215 DEBUG.println("OCSP response type: " + responseType);
216 }
217 throw new IOException("Unsupported OCSP response type: " +
218 responseType);
219 }
220
221 // BasicOCSPResponse
222 DerInputStream basicOCSPResponse =
223 new DerInputStream(derIn.getOctetString());
224
225 DerValue[] seqTmp = basicOCSPResponse.getSequence(2);
226 if (seqTmp.length < 3) {
227 throw new IOException("Unexpected BasicOCSPResponse value");
228 }
229
230 DerValue responseData = seqTmp[0];
231
232 // Need the DER encoded ResponseData to verify the signature later
233 byte[] responseDataDer = seqTmp[0].toByteArray();
234
235 // tbsResponseData
236 if (responseData.tag != DerValue.tag_Sequence) {
237 throw new IOException("Bad encoding in tbsResponseData " +
238 "element of OCSP response: expected ASN.1 SEQUENCE tag.");
239 }
240 DerInputStream seqDerIn = responseData.data;
241 DerValue seq = seqDerIn.getDerValue();
242
243 // version
244 if (seq.isContextSpecific((byte)0)) {
245 // seq[0] is version
246 if (seq.isConstructed() && seq.isContextSpecific()) {
247 //System.out.println ("version is available");
248 seq = seq.data.getDerValue();
249 int version = seq.getInteger();
250 if (seq.data.available() != 0) {
251 throw new IOException("Bad encoding in version " +
252 " element of OCSP response: bad format");
253 }
254 seq = seqDerIn.getDerValue();
255 }
256 }
257
258 // responderID
259 short tag = (byte)(seq.tag & 0x1f);
260 if (tag == NAME_TAG) {
261 if (DEBUG != null) {
262 X500Name responderName = new X500Name(seq.getData());
263 DEBUG.println("OCSP Responder name: " + responderName);
264 }
265 } else if (tag == KEY_TAG) {
266 // Ignore, for now
267 } else {
268 throw new IOException("Bad encoding in responderID element of " +
269 "OCSP response: expected ASN.1 context specific tag 0 or 1");
270 }
271
272 // producedAt
273 seq = seqDerIn.getDerValue();
274 if (DEBUG != null) {
275 Date producedAtDate = seq.getGeneralizedTime();
276 DEBUG.println("OCSP response produced at: " + producedAtDate);
277 }
278
279 // responses
280 DerValue[] singleResponseDer = seqDerIn.getSequence(1);
281 singleResponseMap
282 = new HashMap<CertId, SingleResponse>(singleResponseDer.length);
283 if (DEBUG != null) {
284 DEBUG.println("OCSP number of SingleResponses: "
285 + singleResponseDer.length);
286 }
287 for (int i = 0; i < singleResponseDer.length; i++) {
288 SingleResponse singleResponse
289 = new SingleResponse(singleResponseDer[i]);
290 singleResponseMap.put(singleResponse.getCertId(), singleResponse);
291 }
292
293 // responseExtensions
294 if (seqDerIn.available() > 0) {
295 seq = seqDerIn.getDerValue();
296 if (seq.isContextSpecific((byte)1)) {
297 DerValue[] responseExtDer = seq.data.getSequence(3);
298 for (int i = 0; i < responseExtDer.length; i++) {
299 Extension responseExtension
300 = new Extension(responseExtDer[i]);
301 if (DEBUG != null) {
302 DEBUG.println("OCSP extension: " + responseExtension);
303 }
304 if (responseExtension.getExtensionId().equals(
305 OCSP_NONCE_EXTENSION_OID)) {
306 /*
307 ocspNonce =
308 responseExtension[i].getExtensionValue();
309 */
310 } else if (responseExtension.isCritical()) {
311 throw new IOException(
312 "Unsupported OCSP critical extension: " +
313 responseExtension.getExtensionId());
314 }
315 }
316 }
317 }
318
319 // signatureAlgorithmId
320 AlgorithmId sigAlgId = AlgorithmId.parse(seqTmp[1]);
321
322 // signature
323 byte[] signature = seqTmp[2].getBitString();
324 X509CertImpl[] x509Certs = null;
325
326 // if seq[3] is available , then it is a sequence of certificates
327 if (seqTmp.length > 3) {
328 // certs are available
329 DerValue seqCert = seqTmp[3];
330 if (!seqCert.isContextSpecific((byte)0)) {
331 throw new IOException("Bad encoding in certs element of " +
332 "OCSP response: expected ASN.1 context specific tag 0.");
333 }
334 DerValue[] certs = seqCert.getData().getSequence(3);
335 x509Certs = new X509CertImpl[certs.length];
336 try {
337 for (int i = 0; i < certs.length; i++) {
338 x509Certs[i] = new X509CertImpl(certs[i].toByteArray());
339 }
340 } catch (CertificateException ce) {
341 throw new IOException("Bad encoding in X509 Certificate", ce);
342 }
343 }
344
345 // Check whether the cert returned by the responder is trusted
346 if (x509Certs != null && x509Certs[0] != null) {
347 X509CertImpl cert = x509Certs[0];
348
349 // First check if the cert matches the responder cert which
350 // was set locally.
351 if (cert.equals(responderCert)) {
352 // cert is trusted, now verify the signed response
353
354 // Next check if the cert was issued by the responder cert
355 // which was set locally.
356 } else if (cert.getIssuerX500Principal().equals(
357 responderCert.getSubjectX500Principal())) {
358
359 // Check for the OCSPSigning key purpose
360 try {
361 List<String> keyPurposes = cert.getExtendedKeyUsage();
362 if (keyPurposes == null ||
363 !keyPurposes.contains(KP_OCSP_SIGNING_OID)) {
364 throw new CertPathValidatorException(
365 "Responder's certificate not valid for signing " +
366 "OCSP responses");
367 }
368 } catch (CertificateParsingException cpe) {
369 // assume cert is not valid for signing
370 throw new CertPathValidatorException(
371 "Responder's certificate not valid for signing " +
372 "OCSP responses", cpe);
373 }
374
375 // Check algorithm constraints specified in security property
376 // "jdk.certpath.disabledAlgorithms".
377 AlgorithmChecker algChecker = new AlgorithmChecker(
378 new TrustAnchor(responderCert, null));
379 algChecker.init(false);
380 algChecker.check(cert, Collections.<String>emptySet());
381
382 // check the validity
383 try {
384 if (dateCheckedAgainst == null) {
385 cert.checkValidity();
386 } else {
387 cert.checkValidity(dateCheckedAgainst);
388 }
389 } catch (GeneralSecurityException e) {
390 throw new CertPathValidatorException(
391 "Responder's certificate not within the " +
392 "validity period", e);
393 }
394
395 // check for revocation
396 //
397 // A CA may specify that an OCSP client can trust a
398 // responder for the lifetime of the responder's
399 // certificate. The CA does so by including the
400 // extension id-pkix-ocsp-nocheck.
401 //
402 Extension noCheck =
403 cert.getExtension(PKIXExtensions.OCSPNoCheck_Id);
404 if (noCheck != null) {
405 if (DEBUG != null) {
406 DEBUG.println("Responder's certificate includes " +
407 "the extension id-pkix-ocsp-nocheck.");
408 }
409 } else {
410 // we should do the revocation checking of the
411 // authorized responder in a future update.
412 }
413
414 // verify the signature
415 try {
416 cert.verify(responderCert.getPublicKey());
417 responderCert = cert;
418 // cert is trusted, now verify the signed response
419
420 } catch (GeneralSecurityException e) {
421 responderCert = null;
422 }
423 } else {
424 throw new CertPathValidatorException(
425 "Responder's certificate is not authorized to sign " +
426 "OCSP responses");
427 }
428 }
429
430 // Confirm that the signed response was generated using the public
431 // key from the trusted responder cert
432 if (responderCert != null) {
433 // Check algorithm constraints specified in security property
434 // "jdk.certpath.disabledAlgorithms".
435 AlgorithmChecker.check(responderCert.getPublicKey(), sigAlgId);
436
437 if (!verifyResponse(responseDataDer, responderCert,
438 sigAlgId, signature)) {
439 throw new CertPathValidatorException(
440 "Error verifying OCSP Responder's signature");
441 }
442 } else {
443 // Need responder's cert in order to verify the signature
444 throw new CertPathValidatorException(
445 "Unable to verify OCSP Responder's signature");
446 }
447 }
448
449 /**
450 * Returns the OCSP ResponseStatus.
451 */
452 ResponseStatus getResponseStatus() {
453 return responseStatus;
454 }
455
456 /*
457 * Verify the signature of the OCSP response.
458 * The responder's cert is implicitly trusted.
459 */
460 private boolean verifyResponse(byte[] responseData, X509Certificate cert,
461 AlgorithmId sigAlgId, byte[] signBytes)
462 throws CertPathValidatorException {
463
464 try {
465 Signature respSignature = Signature.getInstance(sigAlgId.getName());
466 respSignature.initVerify(cert);
467 respSignature.update(responseData);
468
469 if (respSignature.verify(signBytes)) {
470 if (DEBUG != null) {
471 DEBUG.println("Verified signature of OCSP Responder");
472 }
473 return true;
474
475 } else {
476 if (DEBUG != null) {
477 DEBUG.println(
478 "Error verifying signature of OCSP Responder");
479 }
480 return false;
481 }
482 } catch (InvalidKeyException ike) {
483 throw new CertPathValidatorException(ike);
484 } catch (NoSuchAlgorithmException nsae) {
485 throw new CertPathValidatorException(nsae);
486 } catch (SignatureException se) {
487 throw new CertPathValidatorException(se);
488 }
489 }
490
491 /**
492 * Returns the SingleResponse of the specified CertId, or null if
493 * there is no response for that CertId.
494 */
495 SingleResponse getSingleResponse(CertId certId) {
496 return singleResponseMap.get(certId);
497 }
498
499 /*
500 * A class representing a single OCSP response.
501 */
502 final static class SingleResponse implements OCSP.RevocationStatus {
503 private final CertId certId;
504 private final CertStatus certStatus;
505 private final Date thisUpdate;
506 private final Date nextUpdate;
507 private final Date revocationTime;
508 private final CRLReason revocationReason;
509 private final Map<String, java.security.cert.Extension> singleExtensions;
510
511 private SingleResponse(DerValue der) throws IOException {
512 if (der.tag != DerValue.tag_Sequence) {
513 throw new IOException("Bad ASN.1 encoding in SingleResponse");
514 }
515 DerInputStream tmp = der.data;
516
517 certId = new CertId(tmp.getDerValue().data);
518 DerValue derVal = tmp.getDerValue();
519 short tag = (byte)(derVal.tag & 0x1f);
520 if (tag == CERT_STATUS_REVOKED) {
521 certStatus = CertStatus.REVOKED;
522 revocationTime = derVal.data.getGeneralizedTime();
523 if (derVal.data.available() != 0) {
524 DerValue dv = derVal.data.getDerValue();
525 tag = (byte)(dv.tag & 0x1f);
526 if (tag == 0) {
527 int reason = dv.data.getEnumerated();
528 // if reason out-of-range just leave as UNSPECIFIED
529 if (reason >= 0 && reason < values.length) {
530 revocationReason = values[reason];
531 } else {
532 revocationReason = CRLReason.UNSPECIFIED;
533 }
534 } else {
535 revocationReason = CRLReason.UNSPECIFIED;
536 }
537 } else {
538 revocationReason = CRLReason.UNSPECIFIED;
539 }
540 // RevokedInfo
541 if (DEBUG != null) {
542 DEBUG.println("Revocation time: " + revocationTime);
543 DEBUG.println("Revocation reason: " + revocationReason);
544 }
545 } else {
546 revocationTime = null;
547 revocationReason = CRLReason.UNSPECIFIED;
548 if (tag == CERT_STATUS_GOOD) {
549 certStatus = CertStatus.GOOD;
550 } else if (tag == CERT_STATUS_UNKNOWN) {
551 certStatus = CertStatus.UNKNOWN;
552 } else {
553 throw new IOException("Invalid certificate status");
554 }
555 }
556
557 thisUpdate = tmp.getGeneralizedTime();
558
559 if (tmp.available() == 0) {
560 // we are done
561 nextUpdate = null;
562 } else {
563 derVal = tmp.getDerValue();
564 tag = (byte)(derVal.tag & 0x1f);
565 if (tag == 0) {
566 // next update
567 nextUpdate = derVal.data.getGeneralizedTime();
568
569 if (tmp.available() == 0) {
570 // we are done
571 } else {
572 derVal = tmp.getDerValue();
573 tag = (byte)(derVal.tag & 0x1f);
574 }
575 } else {
576 nextUpdate = null;
577 }
578 }
579 // singleExtensions
580 if (tmp.available() > 0) {
581 derVal = tmp.getDerValue();
582 if (derVal.isContextSpecific((byte)1)) {
583 DerValue[] singleExtDer = derVal.data.getSequence(3);
584 singleExtensions =
585 new HashMap<String, java.security.cert.Extension>
586 (singleExtDer.length);
587 for (int i = 0; i < singleExtDer.length; i++) {
588 Extension ext = new Extension(singleExtDer[i]);
589 if (DEBUG != null) {
590 DEBUG.println("OCSP single extension: " + ext);
591 }
592 // We don't support any extensions yet. Therefore, if it
593 // is critical we must throw an exception because we
594 // don't know how to process it.
595 if (ext.isCritical()) {
596 throw new IOException(
597 "Unsupported OCSP critical extension: " +
598 ext.getExtensionId());
599 }
600 singleExtensions.put(ext.getId(), ext);
601 }
602 } else {
603 singleExtensions = Collections.emptyMap();
604 }
605 } else {
606 singleExtensions = Collections.emptyMap();
607 }
608
609 long now = System.currentTimeMillis();
610 Date nowPlusSkew = new Date(now + MAX_CLOCK_SKEW);
611 Date nowMinusSkew = new Date(now - MAX_CLOCK_SKEW);
612 if (DEBUG != null) {
613 String until = "";
614 if (nextUpdate != null) {
615 until = " until " + nextUpdate;
616 }
617 DEBUG.println("Response's validity interval is from " +
618 thisUpdate + until);
619 }
620 // Check that the test date is within the validity interval
621 if ((thisUpdate != null && nowPlusSkew.before(thisUpdate)) ||
622 (nextUpdate != null && nowMinusSkew.after(nextUpdate))) {
623
624 if (DEBUG != null) {
625 DEBUG.println("Response is unreliable: its validity " +
626 "interval is out-of-date");
627 }
628 throw new IOException("Response is unreliable: its validity " +
629 "interval is out-of-date");
630 }
631 }
632
633 /*
634 * Return the certificate's revocation status code
635 */
636 @Override public CertStatus getCertStatus() {
637 return certStatus;
638 }
639
640 private CertId getCertId() {
641 return certId;
642 }
643
644 @Override public Date getRevocationTime() {
645 return (Date) revocationTime.clone();
646 }
647
648 @Override public CRLReason getRevocationReason() {
649 return revocationReason;
650 }
651
652 @Override
653 public Map<String, java.security.cert.Extension> getSingleExtensions() {
654 return Collections.unmodifiableMap(singleExtensions);
655 }
656
657 /**
658 * Construct a string representation of a single OCSP response.
659 */
660 @Override public String toString() {
661 StringBuilder sb = new StringBuilder();
662 sb.append("SingleResponse: \n");
663 sb.append(certId);
664 sb.append("\nCertStatus: "+ certStatus + "\n");
665 if (certStatus == CertStatus.REVOKED) {
666 sb.append("revocationTime is " + revocationTime + "\n");
667 sb.append("revocationReason is " + revocationReason + "\n");
668 }
669 sb.append("thisUpdate is " + thisUpdate + "\n");
670 if (nextUpdate != null) {
671 sb.append("nextUpdate is " + nextUpdate + "\n");
672 }
673 return sb.toString();
674 }
675 }
676}