001/* 002 * Copyright 2014-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2014-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2014-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.util.ssl; 037 038 039 040import java.net.InetAddress; 041import java.net.URI; 042import java.util.Collection; 043import java.util.List; 044import java.security.cert.Certificate; 045import java.security.cert.X509Certificate; 046import javax.net.ssl.HostnameVerifier; 047import javax.net.ssl.SSLSession; 048import javax.net.ssl.SSLSocket; 049import javax.security.auth.x500.X500Principal; 050 051import com.unboundid.asn1.ASN1OctetString; 052import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule; 053import com.unboundid.ldap.sdk.DN; 054import com.unboundid.ldap.sdk.Filter; 055import com.unboundid.ldap.sdk.LDAPConnectionOptions; 056import com.unboundid.ldap.sdk.LDAPException; 057import com.unboundid.ldap.sdk.RDN; 058import com.unboundid.ldap.sdk.ResultCode; 059import com.unboundid.util.Debug; 060import com.unboundid.util.NotMutable; 061import com.unboundid.util.NotNull; 062import com.unboundid.util.Nullable; 063import com.unboundid.util.ObjectPair; 064import com.unboundid.util.PropertyManager; 065import com.unboundid.util.StaticUtils; 066import com.unboundid.util.ThreadSafety; 067import com.unboundid.util.ThreadSafetyLevel; 068import com.unboundid.util.args.IPAddressArgumentValueValidator; 069 070import static com.unboundid.util.ssl.SSLMessages.*; 071 072 073 074/** 075 * This class provides an implementation of an {@code SSLSocket} verifier that 076 * will verify that the presented server certificate includes the address to 077 * which the client intended to establish a connection. It will check the CN 078 * attribute of the certificate subject, as well as certain subjectAltName 079 * extensions, including dNSName, uniformResourceIdentifier, and iPAddress. 080 */ 081@NotMutable() 082@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 083public final class HostNameSSLSocketVerifier 084 extends SSLSocketVerifier 085 implements HostnameVerifier 086{ 087 /** 088 * The name of a system property that can be used to specify the default 089 * behavior that the verifier should exhibit when checking certificates that 090 * contain both a CN attribute in the subject DN and a subject alternative 091 * name extension that contains one or more dNSName, 092 * uniformResourceIdentifier, or iPAddress values. Although RFC 6125 section 093 * 6.4.4 indicates that the CN attribute should not be checked in certificates 094 * that have an appropriate subject alternative name extension, LDAP clients 095 * historically treat both sources as equally valid. 096 */ 097 @NotNull public static final String 098 PROPERTY_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT = 099 HostNameSSLSocketVerifier.class.getName() + 100 ".checkCNWhenSubjectAltNameIsPresent"; 101 102 103 104 /** 105 * Indicates whether to check the CN attribute in the peer certificate's 106 * subject DN when that certificate also contains a subject alternative name 107 * extension. 108 */ 109 static final boolean DEFAULT_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT = 110 PropertyManager.getBoolean( 111 PROPERTY_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT, true); 112 113 114 115 // Indicates whether to allow wildcard certificates which contain an asterisk 116 // as the first component of a CN subject attribute or dNSName subjectAltName 117 // extension. 118 private final boolean allowWildcards; 119 120 // Indicates whether to check the CN attribute in the peer certificate's 121 // subject DN if the certificate also contains a subject alternative name 122 // extension that contains at least dNSName, uniformResourceIdentifier, or 123 // iPAddress value. 124 private final boolean checkCNWhenSubjectAltNameIsPresent; 125 126 127 128 /** 129 * Creates a new instance of this {@code SSLSocket} verifier. 130 * 131 * @param allowWildcards Indicates whether to allow wildcard certificates 132 * that contain an asterisk in the leftmost component 133 * of a hostname in the dNSName or 134 * uniformResourceIdentifier of the subject 135 * alternative name extension, or in the CN attribute 136 * of the subject DN. 137 */ 138 public HostNameSSLSocketVerifier(final boolean allowWildcards) 139 { 140 this(allowWildcards, DEFAULT_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT); 141 } 142 143 144 145 /** 146 * Creates a new instance of this {@code SSLSocket} verifier. 147 * 148 * @param allowWildcards 149 * Indicates whether to allow wildcard certificates that contain 150 * an asterisk in the leftmost component of a hostname in the 151 * dNSName or uniformResourceIdentifier of the subject 152 * alternative name extension, or in the CN attribute of the 153 * subject DN. 154 * @param checkCNWhenSubjectAltNameIsPresent 155 * Indicates whether to check the CN attribute in the peer 156 * certificate's subject DN if the certificate also contains a 157 * subject alternative name extension that contains at least one 158 * dNSName, uniformResourceIdentifier, or iPAddress value. 159 * Although RFC 6125 section 6.4.4 indicates that the CN 160 * attribute should not be checked in certificates that have an 161 * appropriate subject alternative name extension, LDAP clients 162 * historically treat both sources as equally valid. 163 */ 164 public HostNameSSLSocketVerifier(final boolean allowWildcards, 165 final boolean checkCNWhenSubjectAltNameIsPresent) 166 { 167 this.allowWildcards = allowWildcards; 168 this.checkCNWhenSubjectAltNameIsPresent = 169 checkCNWhenSubjectAltNameIsPresent; 170 } 171 172 173 174 /** 175 * Verifies that the provided {@code SSLSocket} is acceptable and the 176 * connection should be allowed to remain established. 177 * 178 * @param host The address to which the client intended the connection 179 * to be established. 180 * @param port The port to which the client intended the connection to 181 * be established. 182 * @param sslSocket The {@code SSLSocket} that should be verified. 183 * 184 * @throws LDAPException If a problem is identified that should prevent the 185 * provided {@code SSLSocket} from remaining 186 * established. 187 */ 188 @Override() 189 public void verifySSLSocket(@NotNull final String host, final int port, 190 @NotNull final SSLSocket sslSocket) 191 throws LDAPException 192 { 193 verifySSLSession(host, port, sslSocket.getSession()); 194 } 195 196 197 198 /** 199 * Verifies that the provided {@code SSLSession} is acceptable and the 200 * connection should be allowed to remain established. 201 * 202 * @param host The address to which the client intended the connection 203 * to be established. 204 * @param port The port to which the client intended the connection to 205 * be established. 206 * @param sslSession The SSL session that was negotiated. 207 * 208 * @throws LDAPException If a problem is identified that should prevent the 209 * provided {@code SSLSocket} from remaining 210 * established. 211 */ 212 private void verifySSLSession(@NotNull final String host, final int port, 213 @NotNull final SSLSession sslSession) 214 throws LDAPException 215 { 216 try 217 { 218 // Get the certificates presented during negotiation. The certificates 219 // will be ordered so that the server certificate comes first. 220 if (sslSession == null) 221 { 222 throw new LDAPException(ResultCode.CONNECT_ERROR, 223 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_SESSION.get(host, port)); 224 } 225 226 final Certificate[] peerCertificateChain = 227 sslSession.getPeerCertificates(); 228 if ((peerCertificateChain == null) || (peerCertificateChain.length == 0)) 229 { 230 throw new LDAPException(ResultCode.CONNECT_ERROR, 231 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_PEER_CERTS.get(host, port)); 232 } 233 234 if (peerCertificateChain[0] instanceof X509Certificate) 235 { 236 final StringBuilder certInfo = new StringBuilder(); 237 if (! certificateIncludesHostname(host, 238 (X509Certificate) peerCertificateChain[0], allowWildcards, 239 checkCNWhenSubjectAltNameIsPresent, certInfo)) 240 { 241 throw new LDAPException(ResultCode.CONNECT_ERROR, 242 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_HOSTNAME_NOT_FOUND.get(host, 243 certInfo.toString())); 244 } 245 } 246 else 247 { 248 throw new LDAPException(ResultCode.CONNECT_ERROR, 249 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_PEER_NOT_X509.get(host, port, 250 peerCertificateChain[0].getType())); 251 } 252 } 253 catch (final LDAPException le) 254 { 255 Debug.debugException(le); 256 throw le; 257 } 258 catch (final Exception e) 259 { 260 Debug.debugException(e); 261 throw new LDAPException(ResultCode.CONNECT_ERROR, 262 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_EXCEPTION.get(host, port, 263 StaticUtils.getExceptionMessage(e)), 264 e); 265 } 266 } 267 268 269 270 /** 271 * Determines whether the provided certificate contains the specified 272 * hostname. 273 * 274 * @param host 275 * The address expected to be found in the provided certificate. 276 * @param certificate 277 * The peer certificate to be validated. 278 * @param allowWildcards 279 * Indicates whether to allow wildcard certificates that contain 280 * an asterisk in the leftmost component of a hostname in the 281 * dNSName or uniformResourceIdentifier of the subject 282 * alternative name extension, or in the CN attribute of the 283 * subject DN. 284 * @param checkCNWhenSubjectAltNameIsPresent 285 * Indicates whether to check the CN attribute in the peer 286 * certificate's subject DN if the certificate also contains a 287 * subject alternative name extension that contains at least one 288 * dNSName, uniformResourceIdentifier, or iPAddress value. RFC 289 * 6125 section 6.4.4 indicates that the CN attribute should not 290 * be checked in certificates that have an appropriate subject 291 * alternative name extension, although some clients may expect 292 * CN matching anyway. 293 * @param certInfo 294 * A buffer into which information will be provided about the 295 * provided certificate. 296 * 297 * @return {@code true} if the expected hostname was found in the 298 * certificate, or {@code false} if not. 299 */ 300 static boolean certificateIncludesHostname(@NotNull final String host, 301 @NotNull final X509Certificate certificate, 302 final boolean allowWildcards, 303 final boolean checkCNWhenSubjectAltNameIsPresent, 304 @NotNull final StringBuilder certInfo) 305 { 306 // Check to see if the provided hostname is an IP address. 307 InetAddress hostInetAddress = null; 308 if (IPAddressArgumentValueValidator.isValidNumericIPAddress(host)) 309 { 310 try 311 { 312 hostInetAddress = 313 LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(host); 314 315 // Loopback IP addresses (but not names like "localhost") should be 316 // considered "potentially trustworthy" as per the W3C Secure Contexts 317 // Candidate Recommendation at https://www.w3.org/TR/secure-contexts/. 318 // That means that when connecting over a loopback, we can assume that 319 // the connection is established to the server we intended, even if that 320 // loopback IP address isn't in the certificate's subject alternative 321 // name extension or the CN attribute of the subject DN. 322 if (hostInetAddress.isLoopbackAddress()) 323 { 324 return true; 325 } 326 } 327 catch (final Exception e) 328 { 329 Debug.debugException(e); 330 } 331 } 332 333 334 // Get the subject DN for the certificate and append it to the certInfo 335 // buffer. 336 final String subjectDNString = 337 certificate.getSubjectX500Principal().getName(X500Principal.RFC2253); 338 certInfo.append("subject='"); 339 certInfo.append(subjectDNString); 340 certInfo.append('\''); 341 342 343 // Check to see if the certificate has a subject alternative name extension. 344 // If so, then check its dNSName, uniformResourceLocator, and iPAddress 345 // elements. 346 boolean hasAuthoritativeSubjectAlternativeName = false; 347 try 348 { 349 final Collection<List<?>> subjectAltNames; 350 subjectAltNames = certificate.getSubjectAlternativeNames(); 351 if (subjectAltNames != null) 352 { 353 for (final List<?> l : subjectAltNames) 354 { 355 final Integer type = (Integer) l.get(0); 356 switch (type) 357 { 358 case 2: // dNSName 359 final String dnsName = (String) l.get(1); 360 certInfo.append(" dnsName='"); 361 certInfo.append(dnsName); 362 certInfo.append('\''); 363 364 if (hostnameMatches(host, dnsName, allowWildcards)) 365 { 366 return true; 367 } 368 369 hasAuthoritativeSubjectAlternativeName = true; 370 break; 371 372 case 6: // uniformResourceIdentifier 373 final String uriString = (String) l.get(1); 374 certInfo.append(" uniformResourceIdentifier='"); 375 certInfo.append(uriString); 376 certInfo.append('\''); 377 378 final String uriHost = getHostFromURI(uriString); 379 if (uriHost != null) 380 { 381 if (IPAddressArgumentValueValidator.isValidNumericIPAddress( 382 uriHost)) 383 { 384 if ((hostInetAddress != null) && 385 ipAddressMatches(hostInetAddress, uriHost)) 386 { 387 return true; 388 } 389 } 390 else if (hostnameMatches(host, uriHost, allowWildcards)) 391 { 392 return true; 393 } 394 } 395 396 hasAuthoritativeSubjectAlternativeName = true; 397 break; 398 399 case 7: // iPAddress 400 final String ipAddressString = (String) l.get(1); 401 certInfo.append(" ipAddress='"); 402 certInfo.append(ipAddressString); 403 certInfo.append('\''); 404 405 if ((hostInetAddress != null) && 406 ipAddressMatches(hostInetAddress, ipAddressString)) 407 { 408 return true; 409 } 410 411 hasAuthoritativeSubjectAlternativeName = true; 412 break; 413 } 414 } 415 } 416 } 417 catch (final Exception e) 418 { 419 Debug.debugException(e); 420 } 421 422 423 // If we found an authoritative subject alternative name and we should not 424 // check the subject DN to see if it contains a CN attribute, then indicate 425 // that we didn't find a match. 426 if (hasAuthoritativeSubjectAlternativeName && 427 (! checkCNWhenSubjectAltNameIsPresent)) 428 { 429 return false; 430 } 431 432 433 // Look for any CN attributes in the certificate subject. 434 try 435 { 436 final DN subjectDN = new DN(subjectDNString); 437 for (final RDN rdn : subjectDN.getRDNs()) 438 { 439 final String[] names = rdn.getAttributeNames(); 440 final String[] values = rdn.getAttributeValues(); 441 for (int i=0; i < names.length; i++) 442 { 443 final String lowerName = StaticUtils.toLowerCase(names[i]); 444 if (lowerName.equals("cn") || lowerName.equals("commonname") || 445 lowerName.equals("2.5.4.3")) 446 447 { 448 final String cnValue = values[i]; 449 if (IPAddressArgumentValueValidator. 450 isValidNumericIPAddress(cnValue)) 451 { 452 if ((hostInetAddress != null) && 453 ipAddressMatches(hostInetAddress, cnValue)) 454 { 455 return true; 456 } 457 } 458 else 459 { 460 if (hostnameMatches(host, cnValue, allowWildcards)) 461 { 462 return true; 463 } 464 } 465 } 466 } 467 } 468 } 469 catch (final Exception e) 470 { 471 // This shouldn't happen for a well-formed certificate subject, but we 472 // have to handle it anyway. 473 Debug.debugException(e); 474 } 475 476 477 // If we've gotten here, then we can't consider the hostname a match. 478 return false; 479 } 480 481 482 483 /** 484 * Determines whether the provided client hostname matches the given 485 * hostname from the certificate. 486 * 487 * @param clientHostname 488 * The hostname that the client used when establishing the 489 * connection. 490 * @param certificateHostname 491 * A hostname obtained from the certificate. 492 * @param allowWildcards 493 * Indicates whether to allow wildcard certificates that contain 494 * an asterisk in the leftmost component of a hostname in the 495 * dNSName or uniformResourceIdentifier of the subject 496 * alternative name extension, or in the CN attribute of the 497 * subject DN. 498 * 499 * @return {@code true} if the client hostname is considered a match for the 500 * certificate hostname, or {@code false} if not. 501 */ 502 private static boolean hostnameMatches(@NotNull final String clientHostname, 503 @NotNull final String certificateHostname, 504 final boolean allowWildcards) 505 { 506 // If the provided certificate hostname does not contain any asterisks, 507 // then we just need to do a case-insensitive match. 508 if (! certificateHostname.contains("*")) 509 { 510 return clientHostname.equalsIgnoreCase(certificateHostname); 511 } 512 513 514 // The certificate hostname contains at least one wildcard. See if that's 515 // allowed. 516 if (! allowWildcards) 517 { 518 return false; 519 } 520 521 522 // Get the first component and the remainder for both the client and 523 // certificate hostnames. If the remainder doesn't match, then it's not a 524 // match. 525 final ObjectPair<String,String> clientFirstComponentAndRemainder = 526 getFirstComponentAndRemainder(clientHostname); 527 final ObjectPair<String,String> certificateFirstComponentAndRemainder = 528 getFirstComponentAndRemainder(certificateHostname); 529 if (! clientFirstComponentAndRemainder.getSecond().equalsIgnoreCase( 530 certificateFirstComponentAndRemainder.getSecond())) 531 { 532 return false; 533 } 534 535 536 // If the first component of the certificate hostname is just an asterisk, 537 // then we can consider it a match. 538 final String certificateFirstComponent = 539 certificateFirstComponentAndRemainder.getFirst(); 540 if (certificateFirstComponent.equals("*")) 541 { 542 return true; 543 } 544 545 546 // The filter has wildcard and non-wildcard components. At this point, the 547 // easiest thing to do is to try to create a substring filter to get the 548 // individual components of the filter. 549 final Filter filter; 550 try 551 { 552 filter = Filter.create("(hostname=" + certificateFirstComponent + ')'); 553 if (filter.getFilterType() != Filter.FILTER_TYPE_SUBSTRING) 554 { 555 return false; 556 } 557 } 558 catch (final Exception e) 559 { 560 Debug.debugException(e); 561 return false; 562 } 563 564 565 return CaseIgnoreStringMatchingRule.getInstance().matchesSubstring( 566 new ASN1OctetString(clientFirstComponentAndRemainder.getFirst()), 567 filter.getRawSubInitialValue(), 568 filter.getRawSubAnyValues(), filter.getRawSubFinalValue()); 569 } 570 571 572 573 /** 574 * Separates the provided address into the leftmost component (everything up 575 * to the first period) and the remainder (everything else, including the 576 * first period). If the provided address does not contain any periods, then 577 * the leftmost component will be the entire value and the remainder will be 578 * an empty string. 579 * 580 * @param address The address to be separated into the leftmost component 581 * and the remainder. It must not be {@code null}. 582 * 583 * @return An object pair in which the first element is the leftmost 584 * component of the provided address and the second element is the 585 * remainder of the address. 586 */ 587 @NotNull() 588 private static ObjectPair<String,String> getFirstComponentAndRemainder( 589 @NotNull final String address) 590 { 591 final int periodPos = address.indexOf('.'); 592 if (periodPos < 0) 593 { 594 return new ObjectPair<>(address, ""); 595 } 596 else 597 { 598 return new ObjectPair<>(address.substring(0, periodPos), 599 address.substring(periodPos)); 600 } 601 } 602 603 604 605 /** 606 * Determines whether the provided client IP address matches the IP address 607 * represented by the provided string. 608 * 609 * @param clientIPAddress 610 * The IP address that the client used when establishing the 611 * connection. 612 * @param certificateIPAddressString 613 * The string representation of an IP address obtained from the 614 * certificate. 615 * 616 * @return {@code true} if the client hostname is considered a match for the 617 * certificate hostname, or {@code false} if not. 618 */ 619 private static boolean ipAddressMatches( 620 @NotNull final InetAddress clientIPAddress, 621 @NotNull final String certificateIPAddressString) 622 { 623 final InetAddress certificateIPAddress; 624 try 625 { 626 certificateIPAddress = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER. 627 getByName(certificateIPAddressString); 628 } 629 catch (final Exception e) 630 { 631 Debug.debugException(e); 632 return false; 633 } 634 635 return clientIPAddress.equals(certificateIPAddress); 636 } 637 638 639 640 /** 641 * Extracts the host from the URI with the given string representation. Note 642 * that the Java URI parser doesn't like hostnames that have wildcards, so we 643 * have to handle them specially. 644 * 645 * @param uriString The string representation of the URI to parse. It must 646 * not be {@code null}. 647 * 648 * @return The host extracted from the provided URI, or {@code null} if none 649 * is available (e.g., because the URI is malformed). 650 */ 651 @Nullable() 652 private static String getHostFromURI(@NotNull final String uriString) 653 { 654 final URI uri; 655 try 656 { 657 uri = new URI(uriString); 658 } 659 catch (final Exception e) 660 { 661 Debug.debugException(e); 662 return null; 663 } 664 665 final String uriHost = uri.getHost(); 666 if (uriHost != null) 667 { 668 return uriHost; 669 } 670 671 672 // Java's URI code can't handle hosts with wildcards. See if the provided 673 // URI string looks like it might contain a wildcard. If not, then just 674 // return null. 675 if (! uriString.contains("*")) 676 { 677 return null; 678 } 679 680 681 // If Java was at least able to parse the scheme, and if the URI starts with 682 // that scheme, then we can go ahead with our own parsing attempt. 683 final String scheme = uri.getScheme(); 684 if ((scheme == null) || scheme.isEmpty() || 685 (! uriString.toLowerCase().startsWith(scheme))) 686 { 687 return null; 688 } 689 690 691 // Strip the scheme from the beginning of the URI. Note that the scheme 692 // probably won't contain the "://", so strip that separately. 693 String paredDownURI = uriString.substring(scheme.length()); 694 if (paredDownURI.startsWith("://")) 695 { 696 paredDownURI = paredDownURI.substring(3); 697 } 698 699 700 // If the pared down URI contains a slash (which would separate the hostport 701 // section from the path), then strip that off and everything after it. 702 final int slashPos = paredDownURI.indexOf('/'); 703 if (slashPos >= 0) 704 { 705 paredDownURI = paredDownURI.substring(0, slashPos); 706 } 707 708 709 // If the pared down URI contains a colon (which would separate the host 710 // from the port), then strip that off and everything after it. 711 final int colonPos = paredDownURI.indexOf(':'); 712 if (colonPos >= 0) 713 { 714 paredDownURI = paredDownURI.substring(0, colonPos); 715 } 716 717 718 // If there's anything left, then it should be the host. 719 if (! paredDownURI.isEmpty()) 720 { 721 return paredDownURI; 722 } 723 724 return null; 725 } 726 727 728 729 /** 730 * Verifies that the provided hostname is acceptable for use with the 731 * negotiated SSL session. 732 * 733 * @param hostname The address to which the client intended the connection 734 * to be established. 735 * @param session The SSL session that was established. 736 */ 737 @Override() 738 public boolean verify(@NotNull final String hostname, 739 @NotNull final SSLSession session) 740 { 741 try 742 { 743 verifySSLSession(hostname, session.getPeerPort(), session); 744 return true; 745 } 746 catch (final LDAPException e) 747 { 748 Debug.debugException(e); 749 return false; 750 } 751 } 752}