Trust custom root CA certificate for Home Assistant HTTPS/WSS connections#52
Conversation
…t SSL connections Co-authored-by: thomasneuberger <23504477+thomasneuberger@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds support for trusting a user-supplied custom root CA certificate for Home Assistant HTTPS and WebSocket connections, enabling secure connections to locally-issued TLS endpoints without disabling certificate validation.
Changes:
- Add
CertificateAuthorityPathtoHomeAssistantOptionsand expose it in API configuration. - Register a named
HttpClientwith a customHttpClientHandlerto validate server certs against a custom root CA when configured. - Update Home Assistant HTTP connector to use
IHttpClientFactoryand add WebSocket remote certificate validation support.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| TgHomeBot.SmartHome.HomeAssistant/TgHomeBot.SmartHome.HomeAssistant.csproj | Adds Microsoft.Extensions.Http dependency needed for AddHttpClient/IHttpClientFactory. |
| TgHomeBot.SmartHome.HomeAssistant/HomeAssistantOptions.cs | Introduces optional CertificateAuthorityPath configuration. |
| TgHomeBot.SmartHome.HomeAssistant/Bootstrap.cs | Configures named HttpClient with custom server certificate validation using a custom root CA. |
| TgHomeBot.SmartHome.HomeAssistant/HomeAssistantConnector.cs | Switches from injected HttpClient to IHttpClientFactory + named client. |
| TgHomeBot.SmartHome.HomeAssistant/HomeAssistantMonitor.cs | Loads custom CA once and sets RemoteCertificateValidationCallback for WebSocket connections. |
| TgHomeBot.Api/appsettings.json | Adds CertificateAuthorityPath setting to default configuration. |
Comments suppressed due to low confidence (1)
TgHomeBot.SmartHome.HomeAssistant/HomeAssistantConnector.cs:76
HttpRequestMessage/HttpResponseMessageareIDisposable, but neither is disposed here. In a long-running service this can contribute to resource pressure (especially if exceptions occur before content is fully read). Consider usingusing/await using(orusing var) around the request/response objects.
var message = new HttpRequestMessage(method, url);
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", options.Value.Token);
var httpClient = httpClientFactory.CreateClient(HttpClientName);
var response = await httpClient.SendAsync(message);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
| private readonly X509Certificate2? _caCertificate = string.IsNullOrEmpty(options.Value.CertificateAuthorityPath) | ||
| ? null | ||
| : X509CertificateLoader.LoadCertificateFromFile(options.Value.CertificateAuthorityPath); |
There was a problem hiding this comment.
_caCertificate is loaded once and stored on the monitor, but it is never disposed in Dispose(). Since X509Certificate2 holds unmanaged resources, consider disposing _caCertificate when the monitor is disposed to avoid leaking handles in long-running processes.
| handler.ServerCertificateCustomValidationCallback = (_, cert, chain, errors) => | ||
| { | ||
| if (errors == SslPolicyErrors.None) return true; | ||
| if (chain is null || cert is null) return false; | ||
| chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; | ||
| chain.ChainPolicy.CustomTrustStore.Add(certificate); | ||
| return chain.Build(cert); | ||
| }; |
There was a problem hiding this comment.
The custom certificate validation callback accepts the certificate whenever chain.Build(cert) succeeds, even if SslPolicyErrors includes RemoteCertificateNameMismatch (hostname mismatch). This can disable hostname verification when a custom CA is configured. Consider only overriding validation when the only error is RemoteCertificateChainErrors, and explicitly rejecting name-mismatch / not-available cases.
| if (!string.IsNullOrEmpty(options.CertificateAuthorityPath)) | ||
| { | ||
| var certificate = X509CertificateLoader.LoadCertificateFromFile(options.CertificateAuthorityPath); | ||
| handler.ServerCertificateCustomValidationCallback = (_, cert, chain, errors) => | ||
| { | ||
| if (errors == SslPolicyErrors.None) return true; | ||
| if (chain is null || cert is null) return false; | ||
| chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; | ||
| chain.ChainPolicy.CustomTrustStore.Add(certificate); |
There was a problem hiding this comment.
LoadCertificateFromFile creates an X509Certificate2 that holds unmanaged resources, but the instance is captured by the validation callback and never disposed. Also, because this runs per-handler creation, the CA cert can be reloaded repeatedly (file IO + allocations). Consider loading/caching the CA certificate once (e.g., as a singleton) and reusing it for both HTTP and WebSocket validation, with a clear ownership/disposal strategy.
| _webSocket.Options.RemoteCertificateValidationCallback = (_, cert, chain, errors) => | ||
| { | ||
| if (errors == SslPolicyErrors.None) return true; | ||
| if (chain is null || cert is null) return false; | ||
| chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; | ||
| chain.ChainPolicy.CustomTrustStore.Add(_caCertificate); | ||
| return chain.Build((X509Certificate2)cert); | ||
| }; |
There was a problem hiding this comment.
The WebSocket certificate validation callback has the same issue as the HTTP one: when errors != None it returns chain.Build(...) without preserving hostname verification. If SslPolicyErrors includes RemoteCertificateNameMismatch, this would still return true as long as the chain builds. Only override chain validation when the only failure is RemoteCertificateChainErrors, and fail closed for other SSL policy errors.
Also note that the current URI construction in this method always prefixes with ws (not wss), so when BaseUrl is https://... this callback may never run because the connection would be attempted over plain WS.
| if (chain is null || cert is null) return false; | ||
| chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; | ||
| chain.ChainPolicy.CustomTrustStore.Add(_caCertificate); | ||
| return chain.Build((X509Certificate2)cert); |
There was a problem hiding this comment.
RemoteCertificateValidationCallback provides cert as X509Certificate, so the direct cast to X509Certificate2 can throw at runtime on platforms where the runtime doesn’t pass an X509Certificate2 instance. Consider converting safely (e.g., creating a new X509Certificate2(cert) or using as with a fallback) before calling chain.Build(...).
| return chain.Build((X509Certificate2)cert); | |
| var certificate = cert as X509Certificate2 ?? new X509Certificate2(cert); | |
| return chain.Build(certificate); |
Home Assistant instances secured by a locally-issued certificate were untrusted because there was no way to supply the root CA. This adds an optional
CertificateAuthorityPathconfiguration field that, when set, validates both HTTP and WebSocket connections against the specified CA rather than bypassing validation entirely.Changes
HomeAssistantOptions— adds optionalstring? CertificateAuthorityPathBootstrap.cs— registers a namedHttpClient("HomeAssistant") with a customHttpClientHandler; when the CA path is set, usesX509ChainTrustMode.CustomRootTrustto validate against the provided cert instead of the system storeHomeAssistantConnector— switches from injectedHttpClienttoIHttpClientFactory+ named client (correct pattern for singletons)HomeAssistantMonitor— loads the CA cert once as a field at construction; setsClientWebSocket.Options.RemoteCertificateValidationCallbackfor WSS connectionsTgHomeBot.SmartHome.HomeAssistant.csproj— addsMicrosoft.Extensions.Httppackage referenceConfiguration
Leave
CertificateAuthorityPathempty or omit it to retain existing behaviour. Certificate is loaded viaX509CertificateLoader.LoadCertificateFromFile(non-deprecated .NET 9+ API).Original prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.