Skip to content

Commit c00704d

Browse files
authored
fix: relaxed SNI hostname resolution (#197)
When establishing TLS connections, SNI resolution may fail if the configured altHostname contains `_` or any other characters not allowed by domain name standards (i.e. letters, digits and hyphens). This change introduces a relaxed SNI resolution strategy which ignores the LDH rules completely. Because this change goes hand in hand with auth. via certificates, I was able to reproduce the issue only via UTs. At this point the official Coder releases supports only auth. via API keys.
1 parent 7005d1e commit c00704d

File tree

5 files changed

+497
-6
lines changed

5 files changed

+497
-6
lines changed

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Fixed
6+
7+
- relaxed SNI hostname resolution
8+
59
## 0.6.5 - 2025-09-16
610

711
### Fixed

‎gradle.properties‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.6.5
1+
version=0.6.6
22
group=com.coder.toolbox
33
name=coder-toolbox

‎src/main/kotlin/com/coder/toolbox/util/TLS.kt‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import com.coder.toolbox.settings.ReadOnlyTLSSettings
44
importokhttp3.internal.tls.OkHostnameVerifier
55
importjava.io.File
66
importjava.io.FileInputStream
7+
importjava.net.IDN
78
importjava.net.InetAddress
89
importjava.net.Socket
10+
importjava.nio.charset.StandardCharsets
911
importjava.security.KeyFactory
1012
importjava.security.KeyStore
1113
importjava.security.cert.CertificateException
@@ -18,11 +20,12 @@ import java.util.Locale
1820
importjavax.net.ssl.HostnameVerifier
1921
importjavax.net.ssl.KeyManager
2022
importjavax.net.ssl.KeyManagerFactory
21-
importjavax.net.ssl.SNIHostName
23+
importjavax.net.ssl.SNIServerName
2224
importjavax.net.ssl.SSLContext
2325
importjavax.net.ssl.SSLSession
2426
importjavax.net.ssl.SSLSocket
2527
importjavax.net.ssl.SSLSocketFactory
28+
importjavax.net.ssl.StandardConstants
2629
importjavax.net.ssl.TrustManager
2730
importjavax.net.ssl.TrustManagerFactory
2831
importjavax.net.ssl.X509TrustManager
@@ -83,11 +86,13 @@ fun sslContextFromPEMs(
8386

8487
funcoderSocketFactory(settings:ReadOnlyTLSSettings): SSLSocketFactory{
8588
val sslContext = sslContextFromPEMs(settings.certPath, settings.keyPath, settings.caPath)
86-
if (settings.altHostname.isNullOrBlank()){
89+
90+
val altHostname = settings.altHostname
91+
if (altHostname.isNullOrBlank()){
8792
return sslContext.socketFactory
8893
}
8994

90-
returnAlternateNameSSLSocketFactory(sslContext.socketFactory, settings.altHostname)
95+
returnAlternateNameSSLSocketFactory(sslContext.socketFactory, altHostname)
9196
}
9297

9398
funcoderTrustManagers(tlsCAPath:String?): Array<TrustManager>{
@@ -111,7 +116,7 @@ fun coderTrustManagers(tlsCAPath: String?): Array<TrustManager>{
111116
return trustManagerFactory.trustManagers.map{MergedSystemTrustManger(it asX509TrustManager) }.toTypedArray()
112117
}
113118

114-
classAlternateNameSSLSocketFactory(privatevaldelegate:SSLSocketFactory, private valalternateName:String?) :
119+
classAlternateNameSSLSocketFactory(privatevaldelegate:SSLSocketFactory, private valalternateName:String) :
115120
SSLSocketFactory(){
116121
overridefungetDefaultCipherSuites(): Array<String> = delegate.defaultCipherSuites
117122

@@ -176,12 +181,19 @@ class AlternateNameSSLSocketFactory(private val delegate: SSLSocketFactory, priv
176181

177182
privatefuncustomizeSocket(socket:SSLSocket){
178183
val params = socket.sslParameters
179-
params.serverNames =listOf(SNIHostName(alternateName))
184+
185+
params.serverNames =listOf(RelaxedSNIHostname(alternateName))
180186
socket.sslParameters = params
181187
}
182188
}
183189

190+
privateclassRelaxedSNIHostname(hostname:String) : SNIServerName(
191+
StandardConstants.SNI_HOST_NAME,
192+
IDN.toASCII(hostname, 0).toByteArray(StandardCharsets.UTF_8)
193+
)
194+
184195
classCoderHostnameVerifier(privatevalalternateName:String?) : HostnameVerifier{
196+
185197
overridefunverify(
186198
host:String,
187199
session:SSLSession,
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
packagecom.coder.toolbox.util
2+
3+
importio.mockk.Runs
4+
importio.mockk.every
5+
importio.mockk.just
6+
importio.mockk.mockk
7+
importio.mockk.verify
8+
importjava.net.InetAddress
9+
importjava.net.Socket
10+
importjavax.net.ssl.SSLParameters
11+
importjavax.net.ssl.SSLSocket
12+
importjavax.net.ssl.SSLSocketFactory
13+
importkotlin.test.Test
14+
importkotlin.test.assertEquals
15+
importkotlin.test.assertNotNull
16+
importkotlin.test.assertSame
17+
18+
19+
classAlternateNameSSLSocketFactoryTest{
20+
21+
@Test
22+
fun`createSocket with no parameters should customize socket with alternate name`(){
23+
// Given
24+
val mockFactory = mockk<SSLSocketFactory>()
25+
val mockSocket = mockk<SSLSocket>(relaxed =true)
26+
val mockParams = mockk<SSLParameters>(relaxed =true)
27+
28+
every{mockFactory.createSocket() } returns mockSocket
29+
every{mockSocket.sslParameters } returns mockParams
30+
every{mockSocket.sslParameters = any() } just Runs
31+
32+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "alternate.example.com")
33+
34+
// When
35+
val result = alternateFactory.createSocket()
36+
37+
// Then
38+
verify{mockSocket.sslParameters = any() }
39+
assertSame(mockSocket, result)
40+
}
41+
42+
@Test
43+
fun`createSocket with host and port should customize socket with alternate name`(){
44+
// Given
45+
val mockFactory = mockk<SSLSocketFactory>()
46+
val mockSocket = mockk<SSLSocket>(relaxed =true)
47+
val mockParams = mockk<SSLParameters>(relaxed =true)
48+
49+
every{mockFactory.createSocket("original.com", 443) } returns mockSocket
50+
every{mockSocket.sslParameters } returns mockParams
51+
every{mockSocket.sslParameters = any() } just Runs
52+
53+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "alternate.example.com")
54+
55+
// When
56+
val result = alternateFactory.createSocket("original.com", 443)
57+
58+
// Then
59+
verify{mockSocket.sslParameters = any() }
60+
assertSame(mockSocket, result)
61+
}
62+
63+
@Test
64+
fun`createSocket with host port and local address should customize socket`(){
65+
// Given
66+
val mockFactory = mockk<SSLSocketFactory>()
67+
val mockSocket = mockk<SSLSocket>(relaxed =true)
68+
val mockParams = mockk<SSLParameters>(relaxed =true)
69+
val localHost = mockk<InetAddress>()
70+
71+
every{mockFactory.createSocket("original.com", 443, localHost, 8080) } returns mockSocket
72+
every{mockSocket.sslParameters } returns mockParams
73+
every{mockSocket.sslParameters = any() } just Runs
74+
75+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "alternate.example.com")
76+
77+
// When
78+
val result = alternateFactory.createSocket("original.com", 443, localHost, 8080)
79+
80+
// Then
81+
verify{mockSocket.sslParameters = any() }
82+
assertSame(mockSocket, result)
83+
}
84+
85+
@Test
86+
fun`createSocket with InetAddress should customize socket with alternate name`(){
87+
// Given
88+
val mockFactory = mockk<SSLSocketFactory>()
89+
val mockSocket = mockk<SSLSocket>(relaxed =true)
90+
val mockParams = mockk<SSLParameters>(relaxed =true)
91+
val address = mockk<InetAddress>()
92+
93+
every{mockFactory.createSocket(address, 443) } returns mockSocket
94+
every{mockSocket.sslParameters } returns mockParams
95+
every{mockSocket.sslParameters = any() } just Runs
96+
97+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "alternate.example.com")
98+
99+
// When
100+
val result = alternateFactory.createSocket(address, 443)
101+
102+
// Then
103+
verify{mockSocket.sslParameters = any() }
104+
assertSame(mockSocket, result)
105+
}
106+
107+
@Test
108+
fun`createSocket with InetAddress and local address should customize socket`(){
109+
// Given
110+
val mockFactory = mockk<SSLSocketFactory>()
111+
val mockSocket = mockk<SSLSocket>(relaxed =true)
112+
val mockParams = mockk<SSLParameters>(relaxed =true)
113+
val address = mockk<InetAddress>()
114+
val localAddress = mockk<InetAddress>()
115+
116+
every{mockFactory.createSocket(address, 443, localAddress, 8080) } returns mockSocket
117+
every{mockSocket.sslParameters } returns mockParams
118+
every{mockSocket.sslParameters = any() } just Runs
119+
120+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "alternate.example.com")
121+
122+
// When
123+
val result = alternateFactory.createSocket(address, 443, localAddress, 8080)
124+
125+
// Then
126+
verify{mockSocket.sslParameters = any() }
127+
assertSame(mockSocket, result)
128+
}
129+
130+
@Test
131+
fun`createSocket with existing socket should customize socket with alternate name`(){
132+
// Given
133+
val mockFactory = mockk<SSLSocketFactory>()
134+
val mockSSLSocket = mockk<SSLSocket>(relaxed =true)
135+
val mockParams = mockk<SSLParameters>(relaxed =true)
136+
val existingSocket = mockk<Socket>()
137+
138+
every{mockFactory.createSocket(existingSocket, "original.com", 443, true) } returns mockSSLSocket
139+
every{mockSSLSocket.sslParameters } returns mockParams
140+
every{mockSSLSocket.sslParameters = any() } just Runs
141+
142+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "alternate.example.com")
143+
144+
// When
145+
val result = alternateFactory.createSocket(existingSocket, "original.com", 443, true)
146+
147+
// Then
148+
verify{mockSSLSocket.sslParameters = any() }
149+
assertSame(mockSSLSocket, result)
150+
}
151+
152+
@Test
153+
fun`customizeSocket should set SNI hostname to alternate name for valid hostname`(){
154+
// Given
155+
val mockFactory = mockk<SSLSocketFactory>()
156+
val mockSocket = mockk<SSLSocket>(relaxed =true)
157+
val mockParams = mockk<SSLParameters>(relaxed =true)
158+
159+
every{mockFactory.createSocket() } returns mockSocket
160+
every{mockSocket.sslParameters } returns mockParams
161+
every{mockSocket.sslParameters = any() } just Runs
162+
163+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "valid-hostname.example.com")
164+
165+
// When & Then - This should work without throwing an exception
166+
assertNotNull(alternateFactory.createSocket())
167+
verify{mockSocket.sslParameters = any() }
168+
}
169+
170+
@Test
171+
fun`customizeSocket should NOT throw IllegalArgumentException for hostname with underscore`(){
172+
// Given
173+
val mockFactory = mockk<SSLSocketFactory>()
174+
val mockSocket = mockk<SSLSocket>(relaxed =true)
175+
val mockParams = mockk<SSLParameters>(relaxed =true)
176+
177+
every{mockFactory.createSocket() } returns mockSocket
178+
every{mockSocket.sslParameters } returns mockParams
179+
every{mockSocket.sslParameters = any() } just Runs
180+
181+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "non_compliant_hostname.example.com")
182+
183+
// When & Then - This should work without throwing an exception
184+
assertNotNull(alternateFactory.createSocket())
185+
verify{mockSocket.sslParameters = any() }
186+
assertEquals(0, mockSocket.sslParameters.serverNames.size)
187+
}
188+
189+
@Test
190+
fun`createSocket should work with valid international domain names`(){
191+
// Given
192+
val mockFactory = mockk<SSLSocketFactory>()
193+
val mockSocket = mockk<SSLSocket>(relaxed =true)
194+
val mockParams = mockk<SSLParameters>(relaxed =true)
195+
196+
every{mockFactory.createSocket() } returns mockSocket
197+
every{mockSocket.sslParameters } returns mockParams
198+
every{mockSocket.sslParameters = any() } just Runs
199+
200+
val alternateFactory =AlternateNameSSLSocketFactory(mockFactory, "test-server.example.com")
201+
202+
// When & Then - This should work as hyphens are valid
203+
assertNotNull(alternateFactory.createSocket())
204+
verify{mockSocket.sslParameters = any() }
205+
}
206+
207+
privatefuncreateMockSSLSocketFactory(): SSLSocketFactory{
208+
val mockFactory = mockk<SSLSocketFactory>()
209+
val mockSocket = mockk<SSLSocket>(relaxed =true)
210+
val mockParams = mockk<SSLParameters>(relaxed =true)
211+
212+
// Setup default behavior
213+
every{mockFactory.defaultCipherSuites } returns arrayOf("TLS_AES_256_GCM_SHA384")
214+
every{mockFactory.supportedCipherSuites } returns arrayOf("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256")
215+
216+
// Make all createSocket methods return our mock socket
217+
every{mockFactory.createSocket() } returns mockSocket
218+
every{mockFactory.createSocket(any<String>(), any<Int>()) } returns mockSocket
219+
every{mockFactory.createSocket(any<String>(), any<Int>(), any<InetAddress>(), any<Int>()) } returns mockSocket
220+
every{mockFactory.createSocket(any<InetAddress>(), any<Int>()) } returns mockSocket
221+
every{
222+
mockFactory.createSocket(
223+
any<InetAddress>(),
224+
any<Int>(),
225+
any<InetAddress>(),
226+
any<Int>()
227+
)
228+
} returns mockSocket
229+
every{mockFactory.createSocket(any<Socket>(), any<String>(), any<Int>(), any<Boolean>()) } returns mockSocket
230+
231+
// Setup SSL parameters
232+
every{mockSocket.sslParameters } returns mockParams
233+
every{mockSocket.sslParameters = any() } just Runs
234+
235+
return mockFactory
236+
}
237+
}

0 commit comments

Comments
(0)