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:
git clone git@github.com:Hakky54/certificate-ripper.gitcd certificate-rippergit switch feature/support-for-postgres-dbmvn clean install -Pfat-jardocker 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.keyjava -jar target/crip.jar print -u=postgresql://localhost:5432/ --> will print the certificatemvn clean install -Pnative-image./target/crip print -u=postgresql://localhost:5432/ --> does not print the certificateSo 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}
)
Before addressing your specific issue, I completed the following projects as baseline verification tests:
Self_Cert : Generates server certificates and sets up a PostgreSQL Server Docker container with SSL support.
demo_jdbc_postgresql : Connects using the PostgreSQL JDBC Driver and utilizes CustomSSLSocketFactory to save the server certificate as a new file in a local directory.
demo_PostgresCertExtractor: Uses a network socket approach to extract and save the server certificate to a local directory.
demo_PostgresSslDebugger : Displays detailed information regarding SSL certificates.
custom-debug-security-provider : Establishes a custom Security Provider.
demo_PostgresCertExtractor_custom-debug-security-provider : Integrates and references the custom Security Provider.
ST: Your GitHub project.
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:
Static Analysis Limitation: GraalVM's "closed-world" analysis cannot automatically detect dynamic Java features like Reflection or Service Loading used by PostgreSQL drivers and Security Providers.
Runtime Observation: The native-image-agent tracks the application's behavior during Step B, recording every dynamic call that occurs while connecting to the database.
Metadata Generation: These observations are saved as configuration files in META-INF/native-image, acting as a "map" for the compiler.
Automatic Discovery: During the native build (Step 7), GraalVM automatically reads this directory to include the previously "invisible" code and resources into the binary.
Seamless Execution: This ensures the final Native Image has all the necessary metadata to run the JDBC driver and Security Providers without ClassNotFound errors.
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.
git clone git@github.com:Hakky54/certificate-ripper.gitcd certificate-rippergit switch feature/support-for-postgres-dbStep A: Create a empty folder
mkdir -p src/main/resources/META-INF/native-image
mvn clean install -Pfat-jar....
java -jar target/crip.jar print -u=postgresql://localhost:5432/ --> will print the certificateStep 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)
mvn clean install -Pnative-image./target/crip print -u=postgresql://localhost:5432/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
├── 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