CopilotForXcode icon indicating copy to clipboard operation
CopilotForXcode copied to clipboard

GitHub Copilot for Xcode Fails in Enterprise Environments with MITM Proxy

Open bc-lee opened this issue 10 months ago • 3 comments
trafficstars

Describe the bug In our enterprise environment, HTTPS traffic is intercepted and inspected using a MITM proxy, requiring the installation of a custom root certificate on all machines. While Node.js allows configuring the trust for a custom root certificate using environment variables such as NODE_EXTRA_CA_CERTS=/path/to/certificate.pem, GitHub Copilot for Xcode lacks an equivalent option. This results in the error: unable to get local issuer certificate.

Versions

  • Copilot for Xcode: 0.29.0
  • Xcode: 16.2 (likely not relevant)
  • macOS: 14.7.2 (likely not relevant)

Steps to reproduce

  1. Set up GitHub Copilot for Xcode in an enterprise network using a MITM proxy.
  2. Attempt to use GitHub Copilot for Xcode on a project.
  3. Observe that GitHub Copilot for Xcode fails to make suggestions due to certificate validation issues.

Screenshots N/A

Logs

[2025-01-07T07:12:51.736Z] [info] [GitHubCopilot] [83702] window/logMessage: {
  "message" : "[contentExclusion] Fetching content exclusion policies {\n  params: {\n    repos: '[email protected]:org\/repo.git',\n    scope: 'repo'\n  }\n}",
  "type" : 4
}
[2025-01-07T07:12:51.788Z] [info] [GitHubCopilot] [83702] window/logMessage: {
  "message" : "[contentExclusion] FetchError: unable to get local issuer certificate\n    at fetch (\/snapshot\/copilot-client\/node_modules\/@adobe\/helix-fetch\/src\/fetch\/index.js:99:11)\n    at processTicksAndRejections (node:internal\/process\/task_queues:95:5)\n    at cachingFetch (\/snapshot\/copilot-client\/node_modules\/@adobe\/helix-fetch\/src\/fetch\/index.js:288:16)\n    at zge.fetch (\/snapshot\/copilot-client\/lib\/src\/network\/helix.ts:93:22)\n    at \/snapshot\/copilot-client\/lib\/src\/contentExclusion\/contentExclusions.ts:228:24 {\n  type: 'system',\n  _name: 'FetchError',\n  code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',\n  errno: undefined,\n  erroredSysCall: undefined\n} Error evaluating policy for <file:\/\/\/path\/to\/some\/swift.swift>",
  "type" : 1
}

Additional context In other environments, such as VSCode or IntelliJ IDEA, I can use GitHub Copilot by configuring the custom root certificate. This highlights that the issue is specific to GitHub Copilot for Xcode.

bc-lee avatar Jan 07 '25 07:01 bc-lee

Date: Tue, 7 Jan 2025 21:23:13 +0900
Subject: [PATCH] Add support for specifying extra CA certificates in GitHub Copilot for Xcode

This patch enables users to configure a custom root certificate for
the language server, addressing issues with MITM proxies in
enterprise environments. 
Users can now specify the path to extra CA certificates in the advanced
settings, which will be passed to the language server via
the NODE_EXTRA_CA_CERTS environment variable.

Fixes #95

---
 .../AdvancedSettings/EnterpriseSection.swift    | 17 +++++++++++++++--
 .../LanguageServer/GitHubCopilotService.swift   |  9 +++++----
 Tool/Sources/Preferences/Keys.swift             |  4 ++++
 3 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift b/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift
index bcd0adf..c50bfde 100644
--- a/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift
+++ b/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift
@@ -4,6 +4,7 @@ import Toast
 
 struct EnterpriseSection: View {
     @AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI
+    @AppStorage(\.nodeExtraCaCerts) var nodeExtraCaCerts
     @Environment(\.toast) var toast
 
     var body: some View {
@@ -11,12 +12,17 @@ struct EnterpriseSection: View {
             SettingsTextField(
                 title: "Auth provider URL",
                 prompt: "https://your-enterprise.ghe.com",
-                text: DebouncedBinding($gitHubCopilotEnterpriseURI, handler: urlChanged).binding
+                text: DebouncedBinding($gitHubCopilotEnterpriseURI, handler: enterpriseUrlChanged).binding
+            )
+            SettingsTextField(
+                title: "Node extra CA certs",
+                prompt: "Path to extra CA certs (requires restart)",
+                text: DebouncedBinding($nodeExtraCaCerts, handler: nodeExtraCaCertsChanged).binding
             )
         }
     }
 
-    func urlChanged(_ url: String) {
+    func enterpriseUrlChanged(_ url: String) {
         if !url.isEmpty {
             validateAuthURL(url)
         }
@@ -26,6 +32,13 @@ struct EnterpriseSection: View {
         )
     }
 
+    func nodeExtraCaCertsChanged(_ path: String) {
+        NotificationCenter.default.post(
+            name: .gitHubCopilotShouldRefreshEditorInformation,
+            object: nil
+        )
+    }
+
     func validateAuthURL(_ url: String) {
         let maybeURL = URL(string: url)
         guard let parsedURl = maybeURL else {
diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift
index e6d64c1..be5ca49 100644
--- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift
+++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift
@@ -130,6 +130,8 @@ public class GitHubCopilotBaseService {
             let home = ProcessInfo.processInfo.homePath
             let versionNumber = JSONValue(stringLiteral: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "")
             let xcodeVersion = JSONValue(stringLiteral: SystemInfo().xcodeVersion() ?? "")
+            let nodeExtraCaCerts: String = UserDefaults.shared.value(for: \.nodeExtraCaCerts)
+            let nodeExtraCaCertMap: [String: String] = nodeExtraCaCerts.isEmpty ? [:] : ["NODE_EXTRA_CA_CERTS": nodeExtraCaCerts]
 
             #if DEBUG
             // Use local language server if set and available
@@ -145,13 +147,13 @@ public class GitHubCopilotBaseService {
                 }
             }
             // Set debug port and verbose when running in debug
-            let environment: [String: String] = ["HOME": home, "GH_COPILOT_DEBUG_UI_PORT": "8080", "GH_COPILOT_VERBOSE": "true"]
+            let environment: [String: String] = ["HOME": home, "GH_COPILOT_DEBUG_UI_PORT": "8080", "GH_COPILOT_VERBOSE": "true"].merging(nodeExtraCaCertMap) { _, new in new }
             #else
-            let environment: [String: String] = if UserDefaults.shared.value(for: \.verboseLoggingEnabled) {
+            let environment: [String: String] = (if UserDefaults.shared.value(for: \.verboseLoggingEnabled) {
                 ["HOME": home, "GH_COPILOT_VERBOSE": "true"]
             } else {
                 ["HOME": home]
-            }
+            }).merging(nodeExtraCaCertMap) { _, new in new }
             #endif
 
             let executionParams = Process.ExecutionParameters(
@@ -695,4 +697,3 @@ extension InitializingServer: GitHubCopilotLSP {
         try await sendRequest(endpoint.request)
     }
 }
-
diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift
index 3e4a4c1..0d109fb 100644
--- a/Tool/Sources/Preferences/Keys.swift
+++ b/Tool/Sources/Preferences/Keys.swift
@@ -551,6 +551,10 @@ public extension UserDefaultPreferenceKeys {
         .init(defaultValue: "", key: "GitHubCopilotEnterpriseURI")
     }
 
+    var nodeExtraCaCerts: PreferenceKey<String> {
+        .init(defaultValue: "", key: "NodeExtraCaCerts")
+    }
+
     var verboseLoggingEnabled: PreferenceKey<Bool> {
         .init(defaultValue: false, key: "VerboseLoggingEnabled")
     }
--
2.47.1

bc-lee avatar Jan 07 '25 12:01 bc-lee

Hm, sounds like bad idea.

zkhin avatar Jan 09 '25 00:01 zkhin

locally I also configured MITM proxy to intercept traffic when using Xcode copilot, but I usually disabled Proxy strict SSL and that worked well for me.

Image

testforstephen avatar Apr 11 '25 00:04 testforstephen