fix(ionosphere): disable TLS verify for GIRO to unbreak ionosonde fetch

lgdc.uml.edu's TLS handshake doesn't include the InCommon RSA Server
CA 2 intermediate, and Erlang/OTP's chain decode trips on an ASN.1
table-constraint mismatch even when the intermediate is merged into
the trust store. The data is public, read-only, unauthenticated
ionosonde measurements, so verify_none is scoped to this one client
rather than let the pipeline stay dark.
This commit is contained in:
Graham McIntire 2026-04-22 09:48:55 -05:00
parent b92c1ea68b
commit e8c252932a
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 44 additions and 1 deletions

View file

@ -56,7 +56,7 @@ defmodule Microwaveprop.Ionosphere.GiroClient do
Microwaveprop.Instrument.span([:giro, :fetch], %{station: ursi_code}, fn ->
url = build_url(ursi_code, from_dt, to_dt)
case Req.get(url, [receive_timeout: @default_timeout_ms] ++ req_options()) do
case Req.get(url, [receive_timeout: @default_timeout_ms] ++ merged_req_options()) do
{:ok, %{status: 200, body: body}} when is_binary(body) ->
parse_tabular(body)
@ -192,4 +192,27 @@ defmodule Microwaveprop.Ionosphere.GiroClient do
defp req_options do
Application.get_env(:microwaveprop, :giro_req_options, [])
end
# lgdc.uml.edu doesn't bundle the "InCommon RSA Server CA 2"
# intermediate in its handshake. Adding the intermediate to our trust
# store moves the error from unknown_ca to an Erlang/OTP asn1 table
# constraint mismatch during chain decode — :ssl trips on an
# extension shape it doesn't accept. Scope verify_none here is
# acceptable: GIRO data is public, read-only, unauthenticated
# ionosonde measurements, and the endpoint has no alternative. When
# the test config overrides :giro_req_options with a Req.Test plug,
# that override wins — :plug short-circuits before any TLS work, so
# verify_none never runs in tests.
defp merged_req_options do
case req_options() do
[] -> default_req_options()
overrides -> overrides
end
end
@doc false
@spec default_req_options() :: keyword()
def default_req_options do
[connect_options: [transport_opts: [verify: :verify_none]]]
end
end

View file

@ -66,4 +66,24 @@ defmodule Microwaveprop.Ionosphere.GiroClientTest do
assert row.valid_time == ~U[2026-04-15 18:07:30Z]
end
end
describe "default_req_options/0" do
# lgdc.uml.edu's server doesn't bundle the "InCommon RSA Server CA 2"
# intermediate, and Erlang/OTP's chain-build path trips over an
# ASN.1 table-constraint mismatch even when the intermediate is
# merged into cacerts. The data is public read-only ionosonde
# measurements, so we accept verify_none for this specific endpoint
# rather than keep the pipeline dark. Scope is limited to the GIRO
# client — other HTTPS callers still verify normally.
test "returns transport_opts with verify disabled for the GIRO endpoint" do
opts = GiroClient.default_req_options()
transport_opts =
opts
|> Keyword.fetch!(:connect_options)
|> Keyword.fetch!(:transport_opts)
assert Keyword.get(transport_opts, :verify) == :verify_none
end
end
end