You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CertificateHostChecker.java 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. package com.dmdirc.tls;
  2. import java.security.cert.CertificateParsingException;
  3. import java.security.cert.X509Certificate;
  4. import java.util.Arrays;
  5. import java.util.HashSet;
  6. import java.util.Optional;
  7. import java.util.Set;
  8. import java.util.regex.Pattern;
  9. import java.util.stream.Collectors;
  10. import javax.naming.InvalidNameException;
  11. import javax.naming.ldap.LdapName;
  12. import javax.naming.ldap.Rdn;
  13. /**
  14. * Checks that the host we're connecting to is one specified in a certificate.
  15. *
  16. * <p>Certificates match if any of their subjectAlternateName extensions, or the subject's common name, matches
  17. * the host we're connecting to.
  18. */
  19. public class CertificateHostChecker {
  20. /**
  21. * Checks if the specified certificate is valid for the given hostname.
  22. *
  23. * @param certificate The certificate to check.
  24. * @param host The hostname that was connected to.
  25. * @return True if the certificate covers the given hostname, false otherwise.
  26. */
  27. public boolean isValidFor(final X509Certificate certificate, final String host) {
  28. return getAllNames(certificate).stream().anyMatch(name -> matches(name, host));
  29. }
  30. /**
  31. * Checks if the specified name matches against the host, taking into account wildcards.
  32. *
  33. * <p>Hosts are compared by splitting them into domain parts (test.dmdirc.com becomes [test, dmdirc, com]) and
  34. * comparing each part against the corresponding part of the supplied name. Wildcards may only expand within a
  35. * single part (i.e. *.example.com cannot match foo.bar.example.com).
  36. *
  37. * @param name The name to check.
  38. * @param host The host to check it against.
  39. * @return True if the name matches the host; false otherwise.
  40. */
  41. private boolean matches(final String name, final String host) {
  42. final String[] nameParts = name.split("\\.");
  43. final String[] hostParts = host.split("\\.");
  44. if (nameParts.length != hostParts.length) {
  45. return false;
  46. }
  47. for (int i = 0; i < nameParts.length; i++) {
  48. if (!partMatches(nameParts[i], hostParts[i])) {
  49. return false;
  50. }
  51. }
  52. return true;
  53. }
  54. /**
  55. * Checks if the specified part of the name matches the corresponding part of the host, taking into account
  56. * wildcards.
  57. *
  58. * @param namePart The part of the name to be expanded and checked.
  59. * @param hostPart The corresponding part of the host to check the name against.
  60. * @return True if the name and host parts match; false otherwise.
  61. */
  62. private boolean partMatches(final String namePart, final String hostPart) {
  63. return "*".equals(namePart) || hostPart.toLowerCase().matches(
  64. Arrays.stream(namePart.toLowerCase().split("\\*"))
  65. .map(Pattern::quote)
  66. .collect(Collectors.joining(".*")));
  67. }
  68. /**
  69. * Returns all names for which a certificate is valid.
  70. *
  71. * @param cert The certificate to read.
  72. * @return The names which the certificate covers.
  73. */
  74. private Set<String> getAllNames(final X509Certificate cert) {
  75. final Set<String> names = new HashSet<>();
  76. getCommonName(cert).ifPresent(names::add);
  77. getSubjectAlternateNames(cert).ifPresent(names::addAll);
  78. return names;
  79. }
  80. /**
  81. * Reads the common name (CN) from the certificate's subject.
  82. *
  83. * @param cert The certificate to read.
  84. * @return The common name of the certificate, if present.
  85. */
  86. private Optional<String> getCommonName(final X509Certificate cert) {
  87. try {
  88. final LdapName name = new LdapName(cert.getSubjectX500Principal().getName());
  89. return name.getRdns().stream()
  90. .filter(rdn -> "CN".equalsIgnoreCase(rdn.getType()))
  91. .map(Rdn::getValue)
  92. .map(Object::toString)
  93. .findFirst();
  94. } catch (InvalidNameException ex) {
  95. return Optional.empty();
  96. }
  97. }
  98. /**
  99. * Reads all the subjectAlternateName extensions from the certificate.
  100. *
  101. * @param cert The certificate to read.
  102. * @return The (possibly empty) set of subject alternate names.
  103. */
  104. private Optional<Set<String>> getSubjectAlternateNames(final X509Certificate cert) {
  105. try {
  106. return Optional.ofNullable(cert.getSubjectAlternativeNames())
  107. .map(sans -> sans.stream()
  108. // Filter for GeneralName type 2 (dNSName)
  109. .filter(san -> (Integer) san.get(0) == 2)
  110. // Get the string representation of the value of those names
  111. .map(san -> san.get(1))
  112. .map(Object::toString)
  113. .collect(Collectors.toSet()));
  114. } catch (CertificateParsingException e) {
  115. return Optional.empty();
  116. }
  117. }
  118. }