Skip to content

KB: TLS accepts expired certificate in Java

Josef Cacek edited this page Jul 24, 2023 · 1 revision

Issue

We have TLS enabled and use self-signed certificates. We've realized the connection is established even when the certificate expires.

Reproducer

Generate keystore with an expired certificate:

keytool -genkeypair -keystore expired.p12 \
  -storepass 123456 -keypass 123456 -storetype PKCS12 \
  -validity 365 -startdate "2020/01/01 00:00:00" \
  -keyalg RSA -keysize 2048 -alias localhost -dname "cn=localhost" \
  -ext "SAN=ip:127.0.0.1,ip:::1,dns:localhost,dns:localhost.localdomain"

Use it in both - the keyStore and the trustStore configuration:

<ssl enabled="true">
    <properties>
        <property name="keyStore">expired.p12</property>
        <property name="keyStorePassword">123456</property>
        <property name="trustStore">expired.p12</property>
        <property name="trustStorePassword">123456</property>
    </properties>
</ssl>

Start two Hazelcast Enterprise members with this configuration and TLS handshake debug enabled:

export JAVA_OPTS="-Djavax.net.debug=ssl:handshake"
bin/hz start

We can see in the logs, the certificate is accepted even when already expired:

javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.829 CEST|CertificateMessage.java:1172|Consuming server Certificate handshake message (
"Certificate": {
  "certificate_request_context": "",
  "certificate_list": [  
  {
    "certificate" : {
      "version"            : "v3",
      "serial number"      : "34 CA 70 92",
      "signature algorithm": "SHA256withRSA",
      "issuer"             : "CN=localhost",
      "not before"         : "2020-01-01 24:00:00.000 CET",
      "not  after"         : "2020-12-31 24:00:00.000 CET",
      "subject"            : "CN=localhost",
      "subject public key" : "RSA",
      "extensions"         : [
        {
          ObjectId: 2.5.29.17 Criticality=false
          SubjectAlternativeName [
            IPAddress: 127.0.0.1
            IPAddress: 0:0:0:0:0:0:0:1
            DNSName: localhost
            DNSName: localhost.localdomain
          ]
        },
        {
          ObjectId: 2.5.29.14 Criticality=false
          SubjectKeyIdentifier [
          KeyIdentifier [
          0000: 8E C1 75 14 27 6D B7 64   2F 4E 1A D4 90 54 57 B9  ..u.'m.d/N...TW.
          0010: EF 15 11 CB                                        ....
          ]
          ]
        }
      ]}
    "extensions": {
      <no extension>
    }
  },
]
}
)
javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.829 CEST|SSLExtensions.java:173|Ignore unavailable extension: status_request
javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.837 CEST|CertificateVerify.java:1163|Consuming CertificateVerify handshake message (
"CertificateVerify": {
  "signature algorithm": rsa_pss_rsae_sha256
  "signature": {
    0000: A8 BE 51 E6 4F FA 94 5C   03 7C B0 88 22 9A 06 F7  ..Q.O..\...."...
    0010: DB CE FF 5E 0B C2 51 E5   EA 42 77 AA 5F 71 12 5F  ...^..Q..Bw._q._
    0020: D0 DD 9F 04 8A DB 90 AC   F6 15 B2 DD 1A 1C 01 6B  ...............k
    0030: 1C 9D 68 89 07 B4 D0 DF   A4 AE 5D 1A 48 ED 37 30  ..h.......].H.70
    0040: 34 57 40 A7 A6 11 06 03   4C D9 35 56 D6 56 A8 64  4W@.....L.5V.V.d
    0050: 98 02 62 B8 6A 0A 8F BF   56 FE 0D 19 E9 21 6D 88  ..b.j...V....!m.
    0060: 4D 6B CA C5 5C 24 38 DC   FC 1A A8 8D 20 83 5D F5  Mk..\$8..... .].
    0070: B0 13 F8 1A 4E D5 F6 A4   EB D9 69 28 E6 61 29 51  ....N.....i(.a)Q
    0080: 02 C7 D1 D2 CC F2 C7 70   AF 7B E5 44 99 DA 39 98  .......p...D..9.
    0090: EA 90 0D B9 DF DB 24 46   0D 05 8C 32 12 32 06 C7  ......$F...2.2..
    00A0: DA ED 19 E3 4B 7B B7 0C   22 8C 39 F8 7A 6D 7A 84  ....K...".9.zmz.
    00B0: B1 59 F2 23 68 F1 A0 AD   E7 47 6F 3C A2 DA B1 C4  .Y.#h....Go<....
    00C0: 3D B6 D9 B4 2D 6B 5A C9   C5 E4 1D 3D 0B 9A 48 91  =...-kZ....=..H.
    00D0: C7 6D 26 A7 94 EC 54 16   16 A7 42 3E 35 F3 9F 12  .m&...T...B>5...
    00E0: 5D 1A B4 6E 2B F8 C9 F3   3C 26 BD EC 0F 67 D3 FB  ]..n+...<&...g..
    00F0: 77 CF 00 9A D4 E8 99 69   F1 73 C6 DA F8 FD 4A D8  w......i.s....J.
  }
}
)
javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.838 CEST|Finished.java:898|Consuming server Finished handshake message (
"Finished": {
  "verify data": {
    0000: 60 69 D6 B9 9E 6F 14 6B   06 0C D5 3C 30 11 93 E2  `i...o.k...<0...
    0010: 7F 75 83 7C DA DB 8D DB   69 94 88 F4 2D CB A5 89  .u......i...-...
    0020: 9D BD A2 F3 0B 79 B2 E8   8D D5 51 6B 6F 96 9F 27  .....y....Qko..'
  }'}
)
javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.839 CEST|SSLCipher.java:1866|KeyLimit read side: algorithm = AES/GCM/NOPADDING:KEYUPDATE
countdown value = 137438953472
javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.840 CEST|Finished.java:673|Produced client Finished handshake message (
"Finished": {
  "verify data": {
    0000: 3F 65 4F 7E 23 75 C3 58   F8 E4 B7 25 9C 2F 02 E1  ?eO.#u.X...%./..
    0010: 58 86 8B BD 85 0A 82 0C   3C 11 05 3A 0B 49 1A AF  X.......<..:.I..
    0020: 6F 4C 60 D2 0D 9C B5 89   88 6C 0C 5F 77 DF 16 B5  oL`......l._w...
  }'}
)
javax.net.ssl|DEBUG|41|hz.determined_khayyam.cached.thread-3|2023-07-24 12:43:08.840 CEST|SSLCipher.java:2020|KeyLimit write side: algorithm = AES/GCM/NOPADDING:KEYUPDATE
countdown value = 137438953472
javax.net.ssl|DEBUG|42|hz.determined_khayyam.IO.thread-in-0|2023-07-24 12:43:08.848 CEST|NewSessionTicket.java:328|Consuming NewSessionTicket message (
"NewSessionTicket": {
  "ticket_lifetime"      : "86,400",
  "ticket_age_add"       : "<omitted>",
  "ticket_nonce"         : "01",
  "ticket"               : "68 D9 19 55 AB 78 5F 2E 74 6A 88 DC DB 05 A3 15 EA FE F4 9D D6 F8 23 BC 70 26 0A 83 E6 40 7F CB",
  "extensions"           : [
    <no extension>
  ]
}
)

Resolution

Such behavior is correct ("as designed") if the certificate itself is listed in the truststore of the verifying party.

When the checked certificate is directly listed in the truststore, the certpath validation is skipped in Java:

It doesn't contradict the Section 6 in RFC 5280

As a result, not-yet-valid and expired certificates are accepted (considered valid) when listed directly in the Java truststore.

If you want to have the certificate validity checked during the TLS handshake, then put the issuing CA's certificate into the truststore. Don't place there the verified certificate itself.

Self-signed certificates

We understand it's simple to generate key pairs with self-signed certificates and use them in the truststore directly. We value simplicity. Therefore, we have on our roadmap adding a possibility to force the validity check even for the certificates listed in the truststore. This feature should be available in Hazelcast 5.4.0, and its configuration should look like this:

<ssl enabled="true">
    <properties>
        <property name="forceCertValidation">true</property>
        <property name="keyStore">expired.p12</property>
        <property name="keyStorePassword">123456</property>
        <property name="trustStore">expired.p12</property>
        <property name="trustStorePassword">123456</property>
    </properties>
</ssl>

Resources