Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit8a27010

Browse files
committed
feat: add configuration options to support mtls
adding options to support mtls with the coder server. This supportsadding PEM certs and keys to the tls requests, and also supportsadding a CA cert to the trust store. Also allowing for an alternatehostname that may appear in the certs which is useful for testing orfor non-standard cert usage.
1 parent5e55049 commit8a27010

File tree

5 files changed

+250
-0
lines changed

5 files changed

+250
-0
lines changed

‎src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,34 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
7373
CoderGatewayBundle.message("gateway.connector.settings.header-command.comment")
7474
)
7575
}.layout(RowLayout.PARENT_GRID)
76+
row(CoderGatewayBundle.message("gateway.connector.settings.tls-cert-path.title")) {
77+
textField().resizableColumn().align(AlignX.FILL)
78+
.bindText(state::tlsCertPath)
79+
.comment(
80+
CoderGatewayBundle.message("gateway.connector.settings.tls-cert-path.comment")
81+
)
82+
}.layout(RowLayout.PARENT_GRID)
83+
row(CoderGatewayBundle.message("gateway.connector.settings.tls-key-path.title")) {
84+
textField().resizableColumn().align(AlignX.FILL)
85+
.bindText(state::tlsKeyPath)
86+
.comment(
87+
CoderGatewayBundle.message("gateway.connector.settings.tls-key-path.comment")
88+
)
89+
}.layout(RowLayout.PARENT_GRID)
90+
row(CoderGatewayBundle.message("gateway.connector.settings.tls-ca-path.title")) {
91+
textField().resizableColumn().align(AlignX.FILL)
92+
.bindText(state::tlsCAPath)
93+
.comment(
94+
CoderGatewayBundle.message("gateway.connector.settings.tls-ca-path.comment")
95+
)
96+
}.layout(RowLayout.PARENT_GRID)
97+
row(CoderGatewayBundle.message("gateway.connector.settings.tls-alt-name.title")) {
98+
textField().resizableColumn().align(AlignX.FILL)
99+
.bindText(state::tlsAlternateHostname)
100+
.comment(
101+
CoderGatewayBundle.message("gateway.connector.settings.tls-alt-name.comment")
102+
)
103+
}.layout(RowLayout.PARENT_GRID)
76104
}
77105
}
78106

‎src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import java.nio.file.StandardCopyOption
2222
importjava.security.DigestInputStream
2323
importjava.security.MessageDigest
2424
importjava.util.zip.GZIPInputStream
25+
importjavax.net.ssl.HttpsURLConnection
2526
importjavax.xml.bind.annotation.adapters.HexBinaryAdapter
2627

2728

@@ -104,6 +105,10 @@ class CoderCLIManager @JvmOverloads constructor(
104105
conn.setRequestProperty("If-None-Match","\"$etag\"")
105106
}
106107
conn.setRequestProperty("Accept-Encoding","gzip")
108+
if (connisHttpsURLConnection) {
109+
conn.sslSocketFactory= coderSocketFactory()
110+
conn.hostnameVerifier=CoderHostnameVerifier()
111+
}
107112

108113
try {
109114
conn.connect()

‎src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,47 @@ import com.coder.gateway.sdk.v2.models.Workspace
1414
importcom.coder.gateway.sdk.v2.models.WorkspaceBuild
1515
importcom.coder.gateway.sdk.v2.models.WorkspaceTransition
1616
importcom.coder.gateway.sdk.v2.models.toAgentModels
17+
importcom.coder.gateway.services.CoderSettingsState
1718
importcom.google.gson.Gson
1819
importcom.google.gson.GsonBuilder
1920
importcom.intellij.ide.plugins.PluginManagerCore
2021
importcom.intellij.openapi.components.Service
22+
importcom.intellij.openapi.components.service
2123
importcom.intellij.openapi.extensions.PluginId
2224
importcom.intellij.openapi.util.SystemInfo
2325
importokhttp3.OkHttpClient
26+
importokhttp3.internal.tls.OkHostnameVerifier
2427
importokhttp3.logging.HttpLoggingInterceptor
2528
importorg.zeroturnaround.exec.ProcessExecutor
2629
importretrofit2.Retrofit
2730
importretrofit2.converter.gson.GsonConverterFactory
31+
importjava.io.File
32+
importjava.io.FileInputStream
2833
importjava.net.HttpURLConnection.HTTP_CREATED
34+
importjava.net.InetAddress
35+
importjava.net.Socket
2936
importjava.net.URL
37+
importjava.security.KeyFactory
38+
importjava.security.KeyStore
39+
importjava.security.PrivateKey
40+
importjava.security.cert.CertificateFactory
41+
importjava.security.cert.X509Certificate
42+
importjava.security.spec.InvalidKeySpecException
43+
importjava.security.spec.PKCS8EncodedKeySpec
3044
importjava.time.Instant
45+
importjava.util.Base64
46+
importjava.util.Locale
3147
importjava.util.UUID
48+
importjavax.net.ssl.HostnameVerifier
49+
importjavax.net.ssl.KeyManagerFactory
50+
importjavax.net.ssl.SNIHostName
51+
importjavax.net.ssl.SSLContext
52+
importjavax.net.ssl.SSLSession
53+
importjavax.net.ssl.SSLSocket
54+
importjavax.net.ssl.SSLSocketFactory
55+
importjavax.net.ssl.TrustManagerFactory
56+
importjavax.net.ssl.TrustManager
57+
importjavax.net.ssl.X509TrustManager
3258

3359
@Service(Service.Level.APP)
3460
classCoderRestClientService {
@@ -66,7 +92,11 @@ class CoderRestClient(var url: URL, var token: String,
6692
pluginVersion=PluginManagerCore.getPlugin(PluginId.getId("com.coder.gateway"))!!.version// this is the id from the plugin.xml
6793
}
6894

95+
val socketFactory= coderSocketFactory()
96+
val trustManagers= coderTrustManagers()
6997
httpClient=OkHttpClient.Builder()
98+
.sslSocketFactory(socketFactory, trustManagers[0]asX509TrustManager)
99+
.hostnameVerifier(CoderHostnameVerifier())
70100
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
71101
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("User-Agent","Coder Gateway/${pluginVersion} (${SystemInfo.getOsNameAndVersion()};${SystemInfo.OS_ARCH})").build()) }
72102
.addInterceptor {
@@ -218,3 +248,168 @@ class CoderRestClient(var url: URL, var token: String,
218248
}
219249
}
220250
}
251+
252+
funcoderSocketFactory() :SSLSocketFactory {
253+
val state:CoderSettingsState= service()
254+
255+
if (state.tlsCertPath.isBlank()|| state.tlsKeyPath.isBlank()) {
256+
returnSSLSocketFactory.getDefault()asSSLSocketFactory
257+
}
258+
259+
val certificateFactory=CertificateFactory.getInstance("X.509")
260+
val certInputStream=FileInputStream(state.tlsCertPath)
261+
val certChain= certificateFactory.generateCertificates(certInputStream)
262+
certInputStream.close()
263+
264+
// ideally we would use something like PemReader from BouncyCastle, but
265+
// BC is used by the IDE. This makes using BC very impractical since
266+
// type casting will mismatch due to the different class loaders.
267+
val privateKeyPem=File(state.tlsKeyPath).readText()
268+
val start:Int= privateKeyPem.indexOf("-----BEGIN PRIVATE KEY-----")
269+
val end:Int= privateKeyPem.indexOf("-----END PRIVATE KEY-----", start)
270+
val pemBytes:ByteArray=Base64.getDecoder().decode(
271+
privateKeyPem.substring(start+"-----BEGIN PRIVATE KEY-----".length, end)
272+
.replace("\\s+".toRegex(),"")
273+
)
274+
275+
var privateKey:PrivateKey
276+
try {
277+
val kf=KeyFactory.getInstance("RSA")
278+
val keySpec=PKCS8EncodedKeySpec(pemBytes)
279+
privateKey= kf.generatePrivate(keySpec)
280+
}catch (e:InvalidKeySpecException) {
281+
val kf=KeyFactory.getInstance("EC")
282+
val keySpec=PKCS8EncodedKeySpec(pemBytes)
283+
privateKey= kf.generatePrivate(keySpec)
284+
}
285+
286+
val keyStore=KeyStore.getInstance(KeyStore.getDefaultType())
287+
keyStore.load(null)
288+
certChain.withIndex().forEach {
289+
keyStore.setCertificateEntry("cert${it.index}", it.valueasX509Certificate)
290+
}
291+
keyStore.setKeyEntry("key", privateKey,null, certChain.toTypedArray())
292+
293+
val keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
294+
keyManagerFactory.init(keyStore,null)
295+
296+
val sslContext=SSLContext.getInstance("TLS")
297+
298+
val trustManagers= coderTrustManagers()
299+
sslContext.init(keyManagerFactory.keyManagers, trustManagers,null)
300+
301+
if (state.tlsAlternateHostname.isBlank()) {
302+
return sslContext.socketFactory
303+
}
304+
305+
returnAlternateNameSSLSocketFactory(sslContext.socketFactory, state.tlsAlternateHostname)
306+
}
307+
308+
funcoderTrustManagers() :Array<TrustManager> {
309+
val state:CoderSettingsState= service()
310+
311+
val trustManagerFactory=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
312+
if (state.tlsCAPath.isBlank()) {
313+
// return default trust managers
314+
trustManagerFactory.init(nullasKeyStore?)
315+
return trustManagerFactory.trustManagers
316+
}
317+
318+
319+
val certificateFactory=CertificateFactory.getInstance("X.509")
320+
val caInputStream=FileInputStream(state.tlsCAPath)
321+
val certChain= certificateFactory.generateCertificates(caInputStream)
322+
323+
val truststore=KeyStore.getInstance(KeyStore.getDefaultType())
324+
truststore.load(null)
325+
certChain.withIndex().forEach {
326+
truststore.setCertificateEntry("cert${it.index}", it.valueasX509Certificate)
327+
}
328+
trustManagerFactory.init(truststore)
329+
return trustManagerFactory.trustManagers
330+
}
331+
332+
classAlternateNameSSLSocketFactory(privatevaldelegate:SSLSocketFactory, privatevalalternateName:String) : SSLSocketFactory() {
333+
overridefungetDefaultCipherSuites():Array<String> {
334+
return delegate.defaultCipherSuites
335+
}
336+
337+
overridefungetSupportedCipherSuites():Array<String> {
338+
return delegate.supportedCipherSuites
339+
}
340+
341+
overridefuncreateSocket():Socket {
342+
val socket= delegate.createSocket()asSSLSocket
343+
customizeSocket(socket)
344+
return socket
345+
}
346+
347+
overridefuncreateSocket(host:String?,port:Int):Socket {
348+
val socket= delegate.createSocket(host, port)asSSLSocket
349+
customizeSocket(socket)
350+
return socket
351+
}
352+
353+
overridefuncreateSocket(host:String?,port:Int,localHost:InetAddress?,localPort:Int):Socket {
354+
val socket= delegate.createSocket(host, port, localHost, localPort)asSSLSocket
355+
customizeSocket(socket)
356+
return socket
357+
}
358+
359+
overridefuncreateSocket(host:InetAddress?,port:Int):Socket {
360+
val socket= delegate.createSocket(host, port)asSSLSocket
361+
customizeSocket(socket)
362+
return socket
363+
}
364+
365+
overridefuncreateSocket(address:InetAddress?,port:Int,localAddress:InetAddress?,localPort:Int):Socket {
366+
val socket= delegate.createSocket(address, port, localAddress, localPort)asSSLSocket
367+
customizeSocket(socket)
368+
return socket
369+
}
370+
371+
overridefuncreateSocket(s:Socket?,host:String?,port:Int,autoClose:Boolean):Socket {
372+
val socket= delegate.createSocket(s, host, port, autoClose)asSSLSocket
373+
customizeSocket(socket)
374+
return socket
375+
}
376+
377+
privatefuncustomizeSocket(socket:SSLSocket) {
378+
val params= socket.sslParameters
379+
params.serverNames=listOf(SNIHostName(alternateName))
380+
socket.sslParameters= params
381+
}
382+
}
383+
384+
classCoderHostnameVerifier() : HostnameVerifier {
385+
privateval alternateName:String
386+
387+
init {
388+
val state:CoderSettingsState= service()
389+
this.alternateName= state.tlsAlternateHostname.lowercase(Locale.getDefault())
390+
}
391+
392+
overridefunverify(host:String,session:SSLSession):Boolean {
393+
if (alternateName.isEmpty()) {
394+
returnOkHostnameVerifier.verify(host, session)
395+
}
396+
val certs= session.peerCertificates?:returnfalse
397+
for (certin certs) {
398+
if (cert!isX509Certificate) {
399+
continue
400+
}
401+
val entries= cert.subjectAlternativeNames?:continue
402+
for (entryin entries) {
403+
val kind= entry[0]asInt
404+
if (kind!=2) {// DNS Name
405+
continue
406+
}
407+
val hostname= entry[1]asString
408+
if (hostname.lowercase(Locale.getDefault())== alternateName) {
409+
returntrue
410+
}
411+
}
412+
}
413+
returnfalse
414+
}
415+
}

‎src/main/kotlin/com/coder/gateway/services/CoderSettingsState.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class CoderSettingsState : PersistentStateComponent<CoderSettingsState> {
1919
var enableDownloads:Boolean=true
2020
var enableBinaryDirectoryFallback:Boolean=false
2121
var headerCommand:String=""
22+
var tlsCertPath:String=""
23+
var tlsKeyPath:String=""
24+
var tlsCAPath:String=""
25+
var tlsAlternateHostname:String=""
2226
overridefungetState():CoderSettingsState {
2327
returnthis
2428
}

‎src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,21 @@ gateway.connector.settings.header-command.comment=An external command that \
9393
outputs additional HTTP headers added to all requests. The command must \
9494
output each header as `key=value` on its own line. The following \
9595
environment variables will be available to the process: CODER_URL.
96+
gateway.connector.settings.tls-cert-path.title=Cert Path:
97+
gateway.connector.settings.tls-cert-path.comment=Optionally set this to \
98+
the path of a certificate to use for TLS connections. The certificate \
99+
should be in X.509 PEM format.
100+
gateway.connector.settings.tls-key-path.title=Key Path:
101+
gateway.connector.settings.tls-key-path.comment=Optionally set this to \
102+
the path of the private key that corresponds to the above cert path to use \
103+
for TLS connections. The key should be in X.509 PEM format.
104+
gateway.connector.settings.tls-ca-path.title=CA Path:
105+
gateway.connector.settings.tls-ca-path.comment=Optionally set this to \
106+
the path of a file containing certificates for an alternate certificate \
107+
authority used to verify TLS certs returned by the Coder service. \
108+
The file should be in X.509 PEM format.
109+
gateway.connector.settings.tls-alt-name.title=Alt Hostname:
110+
gateway.connector.settings.tls-alt-name.comment=Optionally set this to \
111+
an alternate hostname used for verifying TLS connections. This is useful \
112+
when the hostname used to connect to the Coder service does not match the \
113+
hostname in the TLS certificate.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp