Skip to content

[Bug] macOS Debugger Fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE on HTTPS #4635

@solbirn

Description

@solbirn

Description

When attempting to debug a .NET Isolated Azure Function on macOS using HTTPS, the debugger fails to attach. The preLaunchTask successfully starts the function host, but the extension's internal health check to the /admin/host/status endpoint times out after 60 seconds, throwing a RestError: UNABLE_TO_VERIFY_LEAF_SIGNATURE.

This occurs even when all local configurations are correct, pointing to a bug in how the extension's runtime environment handles TLS connections on macOS.

Key Findings

A detailed investigation revealed the following:

  1. HTTP Works: Switching the debug task to use HTTP works perfectly, and the debugger attaches instantly.
  2. curl Works: While the extension is failing, a manual curl https://localhost:<port>/admin/host/status command from the terminal succeeds and returns a "Running" state.
  3. Certificate is Trusted: The development certificate has been explicitly trusted via dotnet dev-certs https --trust and is verified as trusted in the macOS Keychain.
  4. No Environmental Blocks: The issue persists with the macOS firewall turned off and no other security software or proxies configured.

Root Cause Analysis

An analysis of the extension's source code shows that the startFuncTask function correctly attempts to disable certificate validation for the localhost health check. It explicitly sets the { rejectUnauthorized: false } option before calling sendRequestWithTimeout.

Despite this correct implementation, the underlying HTTP request still fails with a certificate validation error. This proves that the rejectUnauthorized: false flag is not being honored by the underlying execution stack (either the specific version of Node.js used by VS Code or the @azure/core-rest-pipeline library) on macOS.

Like because rejectUnauthorized is honored by underlying library when it's value is true.

export async function sendRequestWithTimeout(context: IActionContext, options: types.AzExtRequestPrepareOptions, timeout: number, clientInfo: types.AzExtGenericClientInfo): Promise<types.AzExtPipelineResponse> {
    const request: PipelineRequest = createPipelineRequest({
        ...options,
        timeout
    });

    if (options.rejectUnauthorized) {
        request.agent = new HttpsAgent({ rejectUnauthorized: options.rejectUnauthorized });
    }

    const client = await createGenericClient(context, clientInfo, { noRetryPolicy: true, addStatusCodePolicy: true });
    return await client.sendRequest(request);
}

Environment

OS Version: MacOS 15.6 (24G84)
VS Code Version: 1.102.1 (Universal)
Extension Version: 1.18.0 (current)
.NET SDK Version: 9.0.303
Core Tools Version: 4.1.0+7ff2567e43c1ae7471cea7394452c1447661e175 (64-bit)
Function Runtime Version: 4.1040.300.25317

Location:

startFuncTask (/Users/***/Code/vscode-azurefunctions/src/commands/pickFuncProcess.ts:158)

Error:

{
  name: "RestError",
  code: "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
  statusCode: undefined,
  request: {
      url: "https://localhost:<port>/admin/host/status",
      body: undefined,
      headers: {
        _headersMap: {
        },
      },
      method: "GET",
      timeout: 500,
      multipartBody: undefined,
      formData: undefined,
      disableKeepAlive: false,
      proxySettings: undefined,
      streamResponseStatusCodes: undefined,
      withCredentials: false,
      abortSignal: undefined,
      tracingOptions: undefined,
      onUploadProgress: undefined,
      onDownloadProgress: undefined,
      requestId: "d86cf8cc-ebe4-460a-a9f4-33d711eab2ec",
      allowInsecureConnection: true,
      enableBrowserStreams: false,
      agent: undefined,
      tlsSettings: undefined,
    }
}

Error Stack Trace:

RestError: unable to verify the first certificate
    at ClientRequest.<anonymous> (/Users/***/Code/vscode-azurefunctions/node_modules/@azure/core-rest-pipeline/src/nodeHttpClient.ts:241:11)
    at Object.onceWrapper (node:events:633:26)
    at ClientRequest.emit (node:events:518:28)
    at emitErrorEvent (node:_http_client:104:11)
    at TLSSocket.socketErrorListener (node:_http_client:518:5)
    at TLSSocket.emit (node:events:518:28)
    at emitErrorNT (node:internal/streams/destroy:170:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at processTicksAndRejections (node:internal/process/task_queues:90:21)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions