Skip to content

Commit 8182d3c

Browse files
committed
fix(tls): bundle WE1 intermediate CA for GraalVM native binary
api.qtsurfer.net omits the WE1 (Google Trust Services) intermediate from its TLS chain. JVM builds AIA-chase to recover it; GraalVM native images cannot. Bundles tls/google-we1.pem (valid until 2029-02-20) as a classpath resource. At startup, Main.injectBundledIntermediates() loads it, makes it a trust anchor in a composite X509TrustManager, and sets it as the default SSLContext before any outbound connection. HttpClient.newBuilder() picks up SSLContext.getDefault() automatically. Fixes: auth() failed: HTTP 0 on macOS arm64 and Linux amd64 native binaries. Bumps version to 0.3.2.
1 parent c618757 commit 8182d3c

6 files changed

Lines changed: 98 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66

77
## [Unreleased]
88

9+
## [0.3.2] — 2026-06-13
10+
11+
### Fixed 🐛
12+
13+
- **Native binary TLS on macOS/Linux: `auth() failed: HTTP 0`**`api.qtsurfer.net` omits the WE1 intermediate CA from its TLS handshake. GraalVM native images cannot AIA-chase at runtime, causing the TLS handshake to fail silently. The WE1 certificate (Google Trust Services, valid until 2029) is now bundled as a classpath resource and injected as a trust anchor at startup via a composite `X509TrustManager` set as the JVM default `SSLContext`, before the first outbound connection. The fat JAR is unaffected (JVM AIA-chases automatically).
14+
915
## [0.3.1] — 2026-06-12
1016

1117
### Added ✨

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The installer detects your platform and picks the right delivery:
4141
Pin a version or override the destination:
4242

4343
```bash
44-
VERSION=0.3.1 INSTALL_DIR=~/.local/bin \
44+
VERSION=0.3.2 INSTALL_DIR=~/.local/bin \
4545
curl -fsSL https://raw.githubusercontent.com/QTSurfer/mcp-java/main/install.sh | bash
4646
```
4747

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.qtsurfer</groupId>
77
<artifactId>mcp-java</artifactId>
8-
<version>0.3.1</version>
8+
<version>0.3.2</version>
99
<packaging>jar</packaging>
1010
<name>qtsurfer-mcp-java</name>
1111

src/main/java/com/qtsurfer/mcp/Main.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
import org.slf4j.Logger;
1010
import org.slf4j.LoggerFactory;
1111

12+
import java.io.InputStream;
13+
import java.security.KeyStore;
14+
import java.security.cert.CertificateFactory;
15+
import java.security.cert.X509Certificate;
16+
import javax.net.ssl.SSLContext;
17+
import javax.net.ssl.TrustManager;
18+
import javax.net.ssl.TrustManagerFactory;
19+
import javax.net.ssl.X509TrustManager;
20+
1221
/**
1322
* Entry point for the QTSurfer MCP server.
1423
*
@@ -65,6 +74,7 @@ public static void main(String[] args) {
6574
* killing the JVM.
6675
*/
6776
static int run(String[] args) {
77+
injectBundledIntermediates();
6878
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
6979
printUsage();
7080
return 0;
@@ -118,6 +128,68 @@ static AuthenticatedClient authenticate(String apikey, String baseUrl) {
118128
return QTSurfer.auth(apikey, opts);
119129
}
120130

131+
/**
132+
* GraalVM native images cannot AIA-chase at runtime. When the server omits an
133+
* intermediate CA from its TLS chain, the handshake fails with HTTP 0.
134+
* This method loads bundled intermediate certs and adds them as trust anchors
135+
* in a composite SSLContext set as the JVM default, so HttpClient picks them up.
136+
*/
137+
private static void injectBundledIntermediates() {
138+
try {
139+
KeyStore extras = KeyStore.getInstance(KeyStore.getDefaultType());
140+
extras.load(null, null);
141+
int added = 0;
142+
for (String name : new String[]{"google-we1"}) {
143+
try (InputStream in = Main.class.getResourceAsStream("/tls/" + name + ".pem")) {
144+
if (in == null) continue;
145+
X509Certificate cert = (X509Certificate)
146+
CertificateFactory.getInstance("X.509").generateCertificate(in);
147+
extras.setCertificateEntry(name, cert);
148+
added++;
149+
}
150+
}
151+
if (added == 0) return;
152+
153+
TrustManagerFactory platformTmf = TrustManagerFactory.getInstance(
154+
TrustManagerFactory.getDefaultAlgorithm());
155+
platformTmf.init((KeyStore) null);
156+
X509TrustManager platformTm = (X509TrustManager) platformTmf.getTrustManagers()[0];
157+
158+
TrustManagerFactory extrasTmf = TrustManagerFactory.getInstance(
159+
TrustManagerFactory.getDefaultAlgorithm());
160+
extrasTmf.init(extras);
161+
X509TrustManager extrasTm = (X509TrustManager) extrasTmf.getTrustManagers()[0];
162+
163+
SSLContext ctx = SSLContext.getInstance("TLS");
164+
ctx.init(null, new TrustManager[]{new X509TrustManager() {
165+
@Override
166+
public void checkClientTrusted(X509Certificate[] chain, String authType)
167+
throws java.security.cert.CertificateException {
168+
platformTm.checkClientTrusted(chain, authType);
169+
}
170+
171+
@Override
172+
public void checkServerTrusted(X509Certificate[] chain, String authType)
173+
throws java.security.cert.CertificateException {
174+
try {
175+
platformTm.checkServerTrusted(chain, authType);
176+
} catch (java.security.cert.CertificateException e) {
177+
extrasTm.checkServerTrusted(chain, authType);
178+
}
179+
}
180+
181+
@Override
182+
public X509Certificate[] getAcceptedIssuers() {
183+
return platformTm.getAcceptedIssuers();
184+
}
185+
}}, null);
186+
SSLContext.setDefault(ctx);
187+
log.debug("Injected {} bundled intermediate CA(s) into SSLContext", added);
188+
} catch (Exception e) {
189+
log.warn("Failed to inject bundled TLS intermediates: {}", e.getMessage());
190+
}
191+
}
192+
121193
private static void printUsage() {
122194
System.out.print(
123195
"\n"
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"resources": {
33
"includes": [
4-
{"pattern": "\\Qbuild.properties\\E"}
4+
{"pattern": "\\Qbuild.properties\\E"},
5+
{"pattern": "\\Qtls/google-we1.pem\\E"}
56
]
67
}
78
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICjjCCAjOgAwIBAgIQf/NXaJvCTjAtkOGKQb0OHzAKBggqhkjOPQQDAjBQMSQw
3+
IgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkds
4+
b2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMjMxMjEzMDkwMDAwWhcN
5+
MjkwMjIwMTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRy
6+
dXN0IFNlcnZpY2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMB
7+
BwNCAARvzTr+Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4
8+
mMTG6J7/EFmPLCaY9eYmJbsPAvpWo4IBAjCB/zAOBgNVHQ8BAf8EBAMCAYYwHQYD
9+
VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAw
10+
HQYDVR0OBBYEFJB3kjVnxP+ozKnme9mAeXvMk/k4MB8GA1UdIwQYMBaAFFSwe61F
11+
uOJAf/sKbvu+M8k8o4TVMDYGCCsGAQUFBwEBBCowKDAmBggrBgEFBQcwAoYaaHR0
12+
cDovL2kucGtpLmdvb2cvZ3NyNC5jcnQwLQYDVR0fBCYwJDAioCCgHoYcaHR0cDov
13+
L2MucGtpLmdvb2cvci9nc3I0LmNybDATBgNVHSAEDDAKMAgGBmeBDAECATAKBggq
14+
hkjOPQQDAgNJADBGAiEAokJL0LgR6SOLR02WWxccAq3ndXp4EMRveXMUVUxMWSMC
15+
IQDspFWa3fj7nLgouSdkcPy1SdOR2AGm9OQWs7veyXsBwA==
16+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)