javapostgresqlsslgraalvmgraalvm-native-image

Custom Security Provider works in fat jar but not in GraalVM native image while extracting PostgreSQL server certificates


I am trying to extract the public certificate of any PostgreSQL database. I got it working with plain java as a library and as a standalone fat jar, but not in a native-image build with GraalVM. I posted a bug report at GraalVM GitHub page here but I have the feeling it is not a bug. I have the feeling I didn't configured graalvm correctly while using a custom security provider however, I am not sure.

Next to that it sometimes takes years before bug reports can be resolve at their page... Using custom security provider is not something exceptional as some use Bouncy Castle and it has proven to work with GraalVM native image so that is why I have the feeling that I might missed a configuration, however my knowledge on that area is quite low.

I spin up a PostgreSQL database with SSL enabled with this command:

docker run --rm -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:12 -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key

I am using postgresql jdbc driver from here

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.8</version>
</dependency>

I use my own library which provides the utility to capture certificates during the SSL Handshake. It can be found here: GitHub - Ayza And the Maven declaration:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>ayza</artifactId>
    <version>10.0.2</version>
</dependency>

And I am using the following plugins to build the native image with Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
        <finalName>crip</finalName>
        <transformers>
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>${application-main-class}</mainClass>
            </transformer>
        </transformers>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.graalvm.nativeimage</groupId>
    <artifactId>native-image-maven-plugin</artifactId>
    <version>21.2.0</version>
    <executions>
        <execution>
            <goals>
                <goal>native-image</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
    <configuration>
        <skip>false</skip>
        <buildArgs>
            --no-fallback
            -H:EnableURLProtocols=https
            -H:EnableURLProtocols=http
            -H:Name=crip
            -march=compatibility
            --future-defaults=all
            -H:AdditionalSecurityProviders=nl.altindag.ssl.provider.FenixProvider
            -H:AdditionalSecurityServiceTypes=nl.altindag.ssl.provider.FenixProvider
        </buildArgs>
    </configuration>
</plugin>

A small reproducable code snippet would be something like this:

import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.ProviderUtils;
import nl.altindag.ssl.util.TrustManagerUtils;

import javax.net.ssl.X509ExtendedTrustManager;
import java.security.cert.X509Certificate;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class App {

    public static void main(String[] args) {
        List<X509Certificate> capturedCertificates = new CopyOnWriteArrayList<>();
        X509ExtendedTrustManager trustManager = TrustManagerUtils.createCertificateCapturingTrustManager(capturedCertificates);
        SSLFactory sslFactory = SSLFactory.builder()
                .withTrustMaterial(trustManager)
                .build();

        ProviderUtils.configure(sslFactory);

        try (Connection conn = DriverManager.getConnection("jdbc:postgresql://localhost:5432/")) {
            // calling getConnection to trigger the SSL handshake
        } catch (SQLException ignored) {
            
        } finally {
            ProviderUtils.remove();
        }

        System.out.println("Amount of captured certificates: " + capturedCertificates.size());
        System.out.println("Captured certificates: ");
        capturedCertificates.forEach(System.out::println);
    }

}

The output of this main method is:

Amount of captured certificates: 1
Captured certificates: 
[
[
  Version: V3
  Subject: CN=localhost
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 2048 bits
  params: null
  modulus: 31211996126076596147999338027719495623709022913370591887632988720610004032097760016008345790156702342195009365368156959125712771875783177237122222689823196867653884881232812806555219009894942490288929180418493391254446706821512257672335346037892662566871554703869495753177918085413564534189604133485556381243824728959475350934112799133217319962706911642363362829383506782715080237659432175302233892295525841514632241532146162042423493679178952709219542178492548540252419613055282285791992580920922147065723236149506037158805915815773798115281679259390591323530600590552570955839294244330974808478242461235603855182171
  public exponent: 65537
  Validity: [From: Tue Jan 14 03:32:28 CET 2025,
               To: Fri Jan 12 03:32:28 CET 2035]
  Issuer: CN=localhost
  SerialNumber: 26:97:cd:84:a8:93:e2:5d:d3:2c:a0:ea:40:8d:7c:93:bf:06:e4:1d

Certificate Extensions: 3
[1]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:false
  PathLen: undefined
]

[2]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
  DNSName: localhost
]

[3]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: ED 8A 72 6D B7 87 AB 26   5E 6C 75 33 5B C9 BE E8  ..rm...&^lu3[...
0010: 04 6E 1A 06                                        .n..
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 00 EE 2E CB 87 F7 20 FD   B6 36 AE E1 7B 3F AA 8F  ...... ..6...?..
0010: 44 38 42 94 BB 50 77 BE   69 21 CA 2B 4A 2F 90 1B  D8B..Pw.i!.+J/..
0020: 93 B0 3D B7 FA FB DB 56   40 BA F6 20 52 78 FC 0F  ..=....V@.. Rx..
0030: EA DE F1 66 13 6F 91 30   B9 48 6A B8 2A 32 32 FE  ...f.o.0.Hj.*22.
0040: 79 DF C8 DD B2 6D 83 C7   D7 56 04 5D 0F 4B 6B 98  y....m...V.].Kk.
0050: 73 AE C3 5C A5 3F 52 3C   A3 F1 6E CF 6D AF 28 E4  s..\.?R<..n.m.(.
0060: 11 79 97 0D 69 02 D5 77   FF CA 9F B0 F7 ED D4 3F  .y..i..w.......?
0070: 17 ED 65 A4 9E CE 2D 42   C5 37 F5 52 98 D7 D9 C2  ..e...-B.7.R....
0080: 9B E5 91 54 A1 64 4C BA   17 BD 7C 14 B8 F2 51 51  ...T.dL.......QQ
0090: 0D 42 CA 2D 19 82 59 5A   AF BB 8E B4 AA 9C FB 37  .B.-..YZ.......7
00A0: 64 DC F4 78 EA 17 13 3D   07 88 45 2E FB 02 96 68  d..x...=..E....h
00B0: 9B F1 25 AF 6E 85 02 DB   77 5A CF 40 4E 70 5B 62  ..%.n...wZ.@Np[b
00C0: C1 83 15 3F 3E CE BC 32   BB 45 4F E3 AC 44 8E A5  ...?>..2.EO..D..
00D0: 47 02 D6 D4 86 34 A4 19   04 3E B2 7B 8F 72 3F 62  G....4...>...r?b
00E0: 19 02 AF F8 C6 9B 96 14   D1 36 AA D7 74 39 7F C3  .........6..t9..
00F0: AB 49 02 94 CE 96 7C B1   F2 D5 1F 5B A2 73 DE B9  .I.........[.s..

]

I will try to give some context. I am using postgres driver manager to interact with the actual database. When establishing the connection it will create an sslcontext instance under the covers by the postgres library itself, see here. I am intercepting this by adding a custom security provider here. This code snippet creates a custom security provider and inserts it in the first position. Under the cover the following code statements will be executed first and second

With this logic I can easily replace an sslcontext of a different library with a custom one. In this case I provide my own one which is able to capture server certificates. This works when running in my IDE and with a fat-jar. However it does not work when running it with a native executable create with native image. Although I posted a small working app above, this issue is related to my changes to the project Certificate Ripper, the actual code changes can be found here

If you want to try it with the changes from the repository, you can run the following steps:

  1. git clone git@github.com:Hakky54/certificate-ripper.git
  2. cd certificate-ripper
  3. git switch feature/support-for-postgres-db
  4. mvn clean install -Pfat-jar
  5. docker run -d --rm -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:12 -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
  6. java -jar target/crip.jar print -u=postgresql://localhost:5432/ --> will print the certificate
  7. mvn clean install -Pnative-image
  8. ./target/crip print -u=postgresql://localhost:5432/ --> does not print the certificate

So it is actually possible to capture the certificates of postgresql with these snippets. It works in plain java, but not anymore when compiled to a native image. I checked whether the custom Security Provider is present and it is. I am getting the following output:

Fenix version 1.0, CertificateRipper version 1.0, SUN version 25, SunRsaSign version 25, SunEC version 25, SunJSSE version 25, SunJCE version 25, SunSASL version 25, JdkLDAP version 25, JdkSASL version 25, Apple version 25

As you can see it is also at the first position, so it should just work. I just have the feeling that at runtime the custom security provider is not used because it is not at the first place or not available maybe. As I am not sure whether I followed the guidelines correctly as explained here: https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/JCASecurityServices/#provider-registration

Update 1 - alternative without jdbc driver

As @Robert suggested in the comment section I also tried without the Postgresql jdbc driver and just plain code, however then I fail to connect to the server. Below is the code snippet which I used:

import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.TrustManagerUtils;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.X509ExtendedTrustManager;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class App {

    public static void main(String[] args) {
        List<X509Certificate> capturedCertificates = new CopyOnWriteArrayList<>();
        X509ExtendedTrustManager trustManager = TrustManagerUtils.createCertificateCapturingTrustManager(capturedCertificates);
        SSLFactory sslFactory = SSLFactory.builder()
                .withTrustMaterial(trustManager)
                .build();

        try {
            SSLSocket socket = (SSLSocket) sslFactory.getSslSocketFactory().createSocket("localhost", 5432);
            socket.setSoTimeout(10000);
            socket.setUseClientMode(true);
            socket.startHandshake();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

And the following logs are being generated with the stacktrace:

javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.056 CET|SSLCipher.java:423|jdk.tls.keyLimits:  entry = AES/GCM/NoPadding KeyUpdate 2^37. AES/GCM/NOPADDING:KEYUPDATE = 137438953472
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.066 CET|SSLCipher.java:423|jdk.tls.keyLimits:  entry =  ChaCha20-Poly1305 KeyUpdate 2^37. CHACHA20-POLY1305:KEYUPDATE = 137438953472
javax.net.ssl|WARNING|30|main|2025-12-22 24:46:43.159 CET|ServerNameExtension.java:265|Unable to indicate server name
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.159 CET|SSLExtensions.java:272|Ignore, context unavailable extension: server_name
javax.net.ssl|INFO|30|main|2025-12-22 24:46:43.162 CET|AlpnExtension.java:174|No available application protocols
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.162 CET|SSLExtensions.java:272|Ignore, context unavailable extension: application_layer_protocol_negotiation
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.162 CET|SessionTicketExtension.java:350|Stateless resumption supported
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.166 CET|SSLExtensions.java:272|Ignore, context unavailable extension: cookie
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.235 CET|SSLExtensions.java:272|Ignore, context unavailable extension: renegotiation_info
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.235 CET|PreSharedKeyExtension.java:659|No session to resume.
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.235 CET|SSLExtensions.java:272|Ignore, context unavailable extension: pre_shared_key
javax.net.ssl|DEBUG|30|main|2025-12-22 24:46:43.246 CET|ClientHello.java:638|Produced ClientHello handshake message (
"ClientHello": {
  "client version"      : "TLSv1.2",
  "random"              : "2B9ADD1F533E9AFF111BBACDCF738A5FA7CA694973016903E7DE8BB38D2F2A6B",
  "session id"          : "3CBEEDA9D55BFC0FA25D66DF92C37AF4C9E91B5F20607FFEFD7EB86115CDBE11",
  "cipher suites"       : "[TLS_AES_256_GCM_SHA384(0x1302), TLS_AES_128_GCM_SHA256(0x1301), TLS_CHACHA20_POLY1305_SHA256(0x1303), TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC02B), TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256(0xCCA9), TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xC030), TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xCCA8), TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC02F), TLS_DHE_RSA_WITH_AES_256_GCM_SHA384(0x009F), TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xCCAA), TLS_DHE_DSS_WITH_AES_256_GCM_SHA384(0x00A3), TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x009E), TLS_DHE_DSS_WITH_AES_128_GCM_SHA256(0x00A2), TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384(0xC024), TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384(0xC028), TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256(0xC023), TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256(0xC027), TLS_DHE_RSA_WITH_AES_256_CBC_SHA256(0x006B), TLS_DHE_DSS_WITH_AES_256_CBC_SHA256(0x006A), TLS_DHE_RSA_WITH_AES_128_CBC_SHA256(0x0067), TLS_DHE_DSS_WITH_AES_128_CBC_SHA256(0x0040), TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xC00A), TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xC014), TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xC009), TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xC013), TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x0039), TLS_DHE_DSS_WITH_AES_256_CBC_SHA(0x0038), TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x0033), TLS_DHE_DSS_WITH_AES_128_CBC_SHA(0x0032), TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00FF)]",
  "compression methods" : "00",
  "extensions"          : [
    "status_request (5)": {
      "certificate status type": ocsp
      "OCSP status request": {
        "responder_id": <empty>
        "request extensions": {
          <empty>
        }
      }
    },
    "supported_groups (10)": {
      "named groups": [x25519, secp256r1, secp384r1, secp521r1, x448, ffdhe2048, ffdhe3072, ffdhe4096, ffdhe6144, ffdhe8192]
    },
    "ec_point_formats (11)": {
      "formats": [uncompressed]
    },
    "status_request_v2 (17)": {
      "cert status request": {
        "certificate status type": ocsp_multi
        "OCSP status request": {
          "responder_id": <empty>
          "request extensions": {
            <empty>
          }
        }
      }
    },
    "extended_master_secret (23)": {
      <empty>
    },
    "session_ticket (35)": {
      <empty>
    },
    "signature_algorithms (13)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, ed25519, ed448, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha224, rsa_sha224, dsa_sha224, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
    },
    "supported_versions (43)": {
      "versions": [TLSv1.3, TLSv1.2]
    },
    "psk_key_exchange_modes (45)": {
      "ke_modes": [psk_dhe_ke]
    },
    "signature_algorithms_cert (50)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, ed25519, ed448, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha224, rsa_sha224, dsa_sha224, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
    },
    "key_share (51)": {
      "client_shares": [  
        {
          "named group": x25519
          "key_exchange": {
            0000: 9B 70 95 2B 4B 25 76 42   52 BD 2E 09 C8 DA A8 08  .p.+K%vBR.......
            0010: A2 39 04 4C 40 F5 EC 86   33 49 65 71 BD 0F 9F 6E  .9.L@...3Ieq...n
          }
        },
        {
          "named group": secp256r1
          "key_exchange": {
            0000: 04 57 5B 93 97 52 BF 01   91 B1 4B E1 F0 E9 BB B2  .W[..R....K.....
            0010: AB 6F 44 0E 2F BA 39 03   E1 3F 1B 06 E9 10 94 D3  .oD./.9..?......
            0020: AB 02 2F 05 3E A2 A5 FC   4B 81 10 E5 21 76 17 7F  ../.>...K...!v..
            0030: D3 7D 00 2F C4 8C DA 2D   10 C5 4B 83 D2 79 D9 6A  .../...-..K..y.j
            0040: BB 
          }
        },
      ]
    }
  ]
}
)
javax.net.ssl|ERROR|30|main|2025-12-22 24:46:43.251 CET|TransportContext.java:368|Fatal (HANDSHAKE_FAILURE): Couldn't kickstart handshaking (
"throwable" : {
  javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake
    at java.base/sun.security.ssl.SSLSocketImpl.handleEOF(SSLSocketImpl.java:1716)
    at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1514)
    at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1421)
    at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:455)
    at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:426)
    at nl.altindag.certificate.ripper@2.6.2-SNAPSHOT/nl.altindag.crip.App2.main(App2.java:25)
  Caused by: java.io.EOFException: SSL peer shut down incorrectly
    at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:494)
    at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:483)
    at java.base/sun.security.ssl.SSLSocketInputRecord.decode(SSLSocketInputRecord.java:160)
    at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:111)
    at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1506)
    ... 4 more}

)

Solution

  • Before addressing your specific issue, I completed the following projects as baseline verification tests:

    demo_jdbc_postgresql and demo_PostgresCertExtractor can extract server cert.

    here is the reference doc: https://www.graalvm.org/latest/reference-manual/native-image/guides/configure-with-tracing-agent/

    Simply put:

    My JDK version: Oracle GraalVM 25

    $ java -version
    java version "25.0.1" 2025-10-21 LTS
    Java(TM) SE Runtime Environment Oracle GraalVM 25.0.1+8.1 (build 25.0.1+8-LTS-jvmci-b01)
    Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 25.0.1+8.1 (build 25.0.1+8-LTS-jvmci-b01, mixed mode, sharing)
    

    I haven't modified any of the file content of your project; I've simply added two extra steps (Step A, Step B) to your process.

    Step A: Create a empty folder

    mkdir -p src/main/resources/META-INF/native-image
    

    ....

    Step B: run with agentlib: native-image-agent

    Run command:

    java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image -cp "target/libs/*:target/crip.jar" nl.altindag.crip.CertificateRipper print -u=postgresql://localhost:5432/
    

    then it will create reachability-metadata.json at the directory (src/main/resources/META-INF/native-image)

    demo_jdbc_postgresql

    demo_jdbc_postgresql
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── com
            │       └── example
            │           └── PostgresSslTest.java
            └── resources
                └── META-INF
                    └── native-image
                        └── reachability-metadata.json <<Not existing in the init>>
    
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.example</groupId>
        <artifactId>jdbc-postgresql-app</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>jdbc-postgresql-app</name>
        <description>jdbc-postgresql-app project for Spring Boot</description>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.postgresql</groupId>
                <artifactId>postgresql</artifactId>
                <version>42.7.2</version>
            </dependency>
        </dependencies>
    
        <build>
            <finalName>app</finalName>
        </build>
    
        <profiles>
            <profile>
                <id>native</id>
                <build>
                    <plugins>
                        <plugin>
                            <groupId>org.graalvm.buildtools</groupId>
                            <artifactId>native-maven-plugin</artifactId>
                            <version>0.9.28</version>
                            <extensions>true</extensions>
                            <executions>
                                <execution>
                                    <id>build-native</id>
                                    <goals>
                                        <goal>compile-no-fork</goal>
                                    </goals>
                                    <phase>package</phase>
                                </execution>
                            </executions>
                            <configuration>
                                <mainClass>com.example.PostgresSslTest</mainClass>
                                <imageName>jdbc-postgresql-app</imageName>
                                <buildArgs>
                                    <buildArg>--no-fallback</buildArg>
                                    <buildArg>--enable-https</buildArg>
                                    <buildArg>--initialize-at-build-time=sun.security.ssl.SSLSocketImpl</buildArg>
                                    <buildArg>--initialize-at-build-time=sun.security.ssl.SSLContextImpl</buildArg>
                                </buildArgs>
                            </configuration>
                        </plugin>
                    </plugins>
                </build>
            </profile>
        </profiles>
    
    </project>
    
    package com.example;
    
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.TrustManager;
    import javax.net.ssl.X509TrustManager;
    import java.io.FileOutputStream;
    import java.security.cert.X509Certificate;
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.util.Base64;
    import java.util.Properties;
    
    public class PostgresSslTest {
        public static void main(String[] args) {
            // 1. Configure connection information (using localhost or Docker service name)
    
            String url = "jdbc:postgresql://localhost:5432/demodb";
            Properties props = new Properties();
            props.setProperty("user", "demouser");
            props.setProperty("password", "Passw0rd!");
    
            // 2. SSL related configuration
            // props.setProperty("ssl", "false");
            // verify-ca: Verifies whether the server credentials were signed by the specified sslrootcert.
            //props.setProperty("sslmode", "verify-ca");
    
            // Point to ca.crt in the project directory
            // props.setProperty("sslrootcert", "./certs/ca.crt");
    
            // Use a custom SocketFactory to intercept credentials.
            props.setProperty("sslfactory", "com.example.PostgresSslTest$CustomSSLSocketFactory");
    
            // Use try-with-resources to ensure resources are automatically shut down.
            System.out.println("Attempting to establish a TLS secure connection...");
            try (Connection conn = DriverManager.getConnection(url, props)) {
                System.out.println("Connection successful!");
            } catch (Exception e) {
                System.err.println("Connection failed or an error occurred while executing SQL:");
                e.printStackTrace();
            }
        }
    
        // Custom SocketFactory
        public static class CustomSSLSocketFactory extends org.postgresql.ssl.NonValidatingFactory {
            public CustomSSLSocketFactory(String arg) throws Exception {
                super(arg);
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(null, new TrustManager[]{new X509TrustManager() {
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }
    
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                        System.out.println("\n ** Intercepted server credential information **");
                        for (X509Certificate cert : chain) {
                            System.out.println("**** cert start****");
                            System.out.println("Subject: " + cert.getSubjectX500Principal());
                            System.out.println("Issuer: " + cert.getIssuerX500Principal());
                            System.out.println("validity period start: " + cert.getNotBefore());
                            System.out.println("validity period end:" + cert.getNotAfter());
                            System.out.println("Serial Number: " + cert.getSerialNumber());
                            System.out.println("**** cert end ****");
                        }
                        System.out.println("-------------------------------------------");
    
                        //save to file start
                        System.out.println(">>> save to file start");
                        try {
                            // 1. Retrieve the server-side credentials (index 0 is the Leaf Certificate).
                            X509Certificate serverCert = chain[0];
                            System.out.println("Subject: " + serverCert.getSubjectX500Principal());
    
                            // 2. Convert to PEM format
                            byte[] derCert = serverCert.getEncoded();
                            String pemCert = "-----BEGIN CERTIFICATE-----\n" +
                                    Base64.getMimeEncoder(64, new byte[]{(byte) '\n'}).encodeToString(derCert) +
                                    "\n-----END CERTIFICATE-----\n";
    
                            // 3. Write to archive
                            try (FileOutputStream fos = new FileOutputStream("downloaded_server.crt")) {
                                fos.write(pemCert.getBytes());
                            }
    
                            System.out.println("The credentials have been saved to: downloaded_server.crt");
    
                        } catch (Exception e) {
                            System.err.println("Storage failed:" + e.getMessage());
                        }
                        //save to file end
                        System.out.println(">>> save to file end");
                    }
    
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }}, null);
                this.factory = ctx.getSocketFactory();
            }
        } // class CustomSSLSocketFactory
    } //class PostgresSslTest
    

    Logically, you can just retrieve the public key and store it.

    sdk default java 25.0.1-graal
    
    mvn clean package
    
    mvn dependency:copy-dependencies -DoutputDirectory=target/libs
    
    java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
      -cp "target/libs/*:target/app.jar" \
      com.example.PostgresSslTest
    
    demo_jdbc_postgresql
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── com
            │       └── example
            │           └── PostgresSslTest.java
            └── resources
                └── META-INF
                    └── native-image
                        └── reachability-metadata.json
    
    mvn clean package -Pnative
    

    open another terminal , execute the command , to dump tcp. You can refer to this content as a starting point for connecting demo_PostgresCertExtractor.

    sudo tcpdump -i any port 5432 -X
    
    target/jdbc-postgresql-app
    

    it will output downloaded_server.crt

    openssl x509 -in downloaded_server.crt -text -noout
    

    demo_PostgresCertExtractor

    demo_PostgresCertExtractor
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── com
            │       └── example
            │           └── PostgresCertExtractor.java
            └── resources
                └── META-INF
                    └── native-image
                        └── reachability-metadata.json <<Not existing in the init>>
    
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.example</groupId>
        <artifactId>postgres-cert-extractor-app</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>postgres-cert-extractor-app</name>
        <description>postgres-cert-extractor-app project for Spring Boot</description>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        </properties>
    
        <build>
            <finalName>app</finalName>
        </build>
    
        <profiles>
            <profile>
                <id>native</id>
                <build>
                    <plugins>
                        <plugin>
                            <groupId>org.graalvm.buildtools</groupId>
                            <artifactId>native-maven-plugin</artifactId>
                            <version>0.9.28</version>
                            <extensions>true</extensions>
                            <executions>
                                <execution>
                                    <id>build-native</id>
                                    <goals>
                                        <goal>compile-no-fork</goal>
                                    </goals>
                                    <phase>package</phase>
                                </execution>
                            </executions>
                            <configuration>
                                <mainClass>com.example.PostgresCertExtractor</mainClass>
                                <imageName>postgres-extractor</imageName>
                                <buildArgs>
                                    <buildArg>--no-fallback</buildArg>
                                    <buildArg>--enable-https</buildArg>
                                    <buildArg>--initialize-at-build-time=sun.security.ssl.SSLSocketImpl</buildArg>
                                    <buildArg>--initialize-at-build-time=sun.security.ssl.SSLContextImpl</buildArg>
                                </buildArgs>
                            </configuration>
                        </plugin>
                    </plugins>
                </build>
            </profile>
        </profiles>
    
    </project>
    
    package com.example;
    
    import javax.net.ssl.*;
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.FileWriter;
    import java.net.Socket;
    import java.security.cert.Certificate;
    import java.security.cert.X509Certificate;
    import java.util.Base64;
    
    public class PostgresCertExtractor {
    
        public static void main(String[] args) {
            String host = "localhost";
            int port = 5432;
            String outputFile = "server.crt";
    
            try (Socket socket = new Socket(host, port)) {
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());
                DataInputStream in = new DataInputStream(socket.getInputStream());
    
                // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-SSL
                /* To initiate an SSL-encrypted connection, the frontend initially sends an SSLRequest message rather than a StartupMessage. The server then responds with a single byte containing S or N, indicating that it is willing or unwilling to perform SSL, respectively. 
                */
                // 1.Send a PostgreSQL SSLRequest packet (length 8, content 1234.5679).
                // This is a protocol step specific to PostgreSQL.
                
                out.writeInt(8);
                out.writeInt(80877103); 
                //https://www.postgresql.org/message-id/160042095164.14056.845885391728089260%40wrigleys.postgresql.org
                /*
                 it says for SSLRequest packet: Int32(80877103)
                */
                out.flush();
    
                // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-SSL
                // 2. Read the server response (1 byte)
                byte response = in.readByte();
                if (response != 'S') {
                    System.err.println("The server does not support SSL or refuses to connect.");
                    return;
                }
    
                // 3. Upgrade a regular socket to an SSL socket
                // We use a TrustManager that "trusts everything" 
                // because our goal is to "extract" rather than "verify".
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                    public void checkClientTrusted(X509Certificate[] xcs, String string) {
                    }
    
                    public void checkServerTrusted(X509Certificate[] xcs, String string) {
                    }
    
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }
                }}, new java.security.SecureRandom());
    
                SSLSocketFactory factory = sslContext.getSocketFactory();
                try (SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, host, port, true)) {
    
                    // Triggering the TLS handshake
                    sslSocket.startHandshake();
    
                    // 4. Obtain server credential chain
                    Certificate[] certs = sslSocket.getSession().getPeerCertificates();
                    if (certs.length > 0 && certs[0] instanceof X509Certificate) {
                        X509Certificate serverCert = (X509Certificate) certs[0];
    
                        // 5. Convert the voucher to PEM format and save it.
                        saveCertAsPem(serverCert, outputFile);
                        System.out.println("The voucher has been successfully retrieved and saved to: " + outputFile);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static void saveCertAsPem(X509Certificate cert, String filePath) throws Exception {
            Base64.Encoder encoder = Base64.getMimeEncoder(64, new byte[]{'\n'});
            String encodedCert = encoder.encodeToString(cert.getEncoded());
    
            try (FileWriter fw = new FileWriter(filePath)) {
                fw.write("-----BEGIN CERTIFICATE-----\n");
                fw.write(encodedCert);
                fw.write("\n-----END CERTIFICATE-----\n");
            }
        }
    }
    

    Logically, you can just retrieve the public key and store it.

    sdk default java 25.0.1-graal
    
    mvn clean package
    
    mvn dependency:copy-dependencies -DoutputDirectory=target/libs
    
    java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
      -cp "target/libs/*:target/app.jar" \
      com.example.PostgresCertExtractor
    
    demo_PostgresCertExtractor
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── com
            │       └── example
            │           └── PostgresCertExtractor.java
            └── resources
                └── META-INF
                    └── native-image
                        └── reachability-metadata.json
    
    mvn clean package -Pnative
    
    target/postgres-extractor
    

    it will output server.crt

    openssl x509 -in server.crt -text -noout