Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip expired certs in partial chain hook #328

Merged
merged 4 commits into from
Sep 2, 2021

Conversation

voltone
Copy link
Contributor

@voltone voltone commented Aug 29, 2021

Fixes #327.

Opened as draft since I want to review the changes once more after clearing my head a bit, but it seems to work in testing...

BTW, note that on OTP 23.3.4.5 and 24.0.4 and later, partial_chain is no longer necessary at all. See https://blog.voltone.net/post/30. But I suppose it will be a good number of years before Mint can drop the partial_chain implementation.

@voltone
Copy link
Contributor Author

voltone commented Aug 30, 2021

Demonstrating the fix, starting by reproducing the problem described in #327:

$ docker run -it --rm hexpm/elixir:1.12.2-erlang-23.3.4-ubuntu-focal-20210325                Mon Aug 30 09:07:30 2021
root@1d8f3da3f4da:/# apt update && apt install -y faketime ca-certificates git-core
[...snip...]
done.
root@1d8f3da3f4da:/# faketime '2021-10-01 09:00' iex
Erlang/OTP 23 [erts-11.2.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Mix.install([:castore, :mint], force: true)
[...snip...]
:ok
iex(2)> isrg = File.read!("/etc/ssl/certs/ISRG_Root_X1.pem") |> :public_key.pem_decode() |> hd() |> elem(1)
<<48, 130, 5, 107, 48, 130, 3, 83, 160, 3, 2, 1, 2, 2, 17, 0, 130, 16, 207, 176,
  210, 64, 227, 89, 68, 99, 224, 187, 99, 130, 139, 0, 48, 13, 6, 9, 42, 134,
  72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 79, 49, ...>>
iex(3)> dst = File.read!("/etc/ssl/certs/DST_Root_CA_X3.pem") |> :public_key.pem_decode() |> hd() |> elem(1)
<<48, 130, 3, 74, 48, 130, 2, 50, 160, 3, 2, 1, 2, 2, 16, 68, 175, 176, 128,
  214, 163, 39, 186, 137, 48, 57, 134, 46, 248, 64, 107, 48, 13, 6, 9, 42, 134,
  72, 134, 247, 13, 1, 1, 5, 5, 0, 48, 63, 49, 36, ...>>
iex(4)> Mint.HTTP.connect(:https, "blog.voltone.net", 443, transport_opts: [reuse_sessions: false, cacerts: [isrg, dst]])

09:00:47.995 [info]  TLS :client: In state :certify at ssl_handshake.erl:1878 generated CLIENT ALERT: Fatal - Certificate Expired

{:error,
 %Mint.TransportError{
   reason: {:tls_alert,
    {:certificate_expired,
     'TLS client: In state certify at ssl_handshake.erl:1878 generated CLIENT ALERT: Fatal - Certificate Expired\n'}}
 }}

Here, Erlang/OTP selects the longest chain (with DST Root CA X3 as the root) and passes it to Mint's partial_chain. Because the public key of the first certificate matches a certificate in the trust store (the DST Root CA X3 certificate itself), Mint returns it as the trusted CA. Erlang/OTP then performs path validation with the entire chain, which fails on 23.3 or later because the first certificate in the chain has expired.

In the same container, starting a new IEx session with the branch from this PR:

root@1d8f3da3f4da:/# faketime '2021-10-01 09:00' iex
Erlang/OTP 23 [erts-11.2.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Mix.install([:castore, {:mint, git: "https://github.com/voltone/mint", branch: "partial-chain-skip-expired"}], force: true)
[...snip...]
:ok
iex(2)> isrg = File.read!("/etc/ssl/certs/ISRG_Root_X1.pem") |> :public_key.pem_decode() |> hd() |> elem(1)
<<48, 130, 5, 107, 48, 130, 3, 83, 160, 3, 2, 1, 2, 2, 17, 0, 130, 16, 207, 176,
  210, 64, 227, 89, 68, 99, 224, 187, 99, 130, 139, 0, 48, 13, 6, 9, 42, 134,
  72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 79, 49, ...>>
iex(3)> dst = File.read!("/etc/ssl/certs/DST_Root_CA_X3.pem") |> :public_key.pem_decode() |> hd() |> elem(1)
<<48, 130, 3, 74, 48, 130, 2, 50, 160, 3, 2, 1, 2, 2, 16, 68, 175, 176, 128,
  214, 163, 39, 186, 137, 48, 57, 134, 46, 248, 64, 107, 48, 13, 6, 9, 42, 134,
  72, 134, 247, 13, 1, 1, 5, 5, 0, 48, 63, 49, 36, ...>>
iex(4)> Mint.HTTP.connect(:https, "blog.voltone.net", 443, transport_opts: [reuse_sessions: false, cacerts: [isrg, dst]])
{:ok,
 %Mint.HTTP1{
  # ...snip...
 }}

Now Mint skips over the expired DST Root CA X3 in the chain, and starts looking for a trusted CA from the cross-signed ISRG Root X1 CA sent by the server. Since the public key of this certificate matches the ISRG Root X1 public key in the trust store, Mint returns it as the trusted CA. This time Erlang/OTP is happy with the (shortened) chain, since all certificates are valid.

Verification still works once the DST Root CA X3 certificate is removed from the trust store:

iex(5)> Mint.HTTP.connect(:https, "blog.voltone.net", 443, transport_opts: [reuse_sessions: false, cacerts: [isrg]]) {:ok,
 %Mint.HTTP1{
  # ...snip...
 }}

@ericmj
Copy link
Member

ericmj commented Aug 30, 2021

This is great, thank you!

I have done an initial review and it looks good. Let us know when it's ready for final review.

💜💜💜💜💜💜

@voltone voltone marked this pull request as ready for review August 31, 2021 05:15
@voltone
Copy link
Contributor Author

voltone commented Aug 31, 2021

Let us know when it's ready for final review.

I suppose it is. I will run a few more manual tests like the one above on other OTP versions, just to be sure. Should be able to do that later today...

@voltone
Copy link
Contributor Author

voltone commented Aug 31, 2021

Ok, additional testing with the following OTP versions was successful:

  • 23.2.7 (pre-23.3)
  • 23.3.4.5 (with alternate chain support in OTP)
  • 24.0.3 (the 24.x equivalent of 23.3.4 as tested above)
  • 24.0.4 (with alternate chain support in OTP)

Of these versions, only 24.0.3 requires the fix in this PR. The other versions do not require the fix; they were tested for regressions.

@whatyouhide
Copy link
Contributor

whatyouhide commented Aug 31, 2021

@voltone did you do the testing manually? Do you think it would be beneficial to add those tests on those versions to CI?

@voltone
Copy link
Contributor Author

voltone commented Aug 31, 2021

Yes, the end-to-end tests were done manually, using my blog as the server. Testing them in CI would require a stable endpoint that presents a suitable chain, and that will continue to do so indefinitely. Most servers with a Let's Encrypt certificate would probably work, at least for the time being, however:

  1. For tests to work between now and Oct 1st, the use of faketime would be needed
  2. If faketime is used after Oct 1st, at some point the server cert will become invalid because it will have been issued in the future

How about I create a PR after Oct 1st that will check for regressions, without faketime?

@ericmj
Copy link
Member

ericmj commented Aug 31, 2021

How about I create a PR after Oct 1st that will check for regressions, without faketime?

That sounds like the most practical solution.

lib/mint/core/transport/ssl.ex Show resolved Hide resolved
test/mint/core/transport/ssl_test.exs Outdated Show resolved Hide resolved
voltone and others added 2 commits August 31, 2021 17:37
Co-authored-by: Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>
@whatyouhide whatyouhide merged commit 0fdf1e0 into elixir-mint:main Sep 2, 2021
@voltone
Copy link
Contributor Author

voltone commented Sep 12, 2021

Hey @ericmj @whatyouhide, will you be publishing a new release of Mint before the end of the month, in case people need it to resolve issues as a result of the DST Root CA expiry?

@ericmj
Copy link
Member

ericmj commented Sep 12, 2021

Done!

Thanks again for your PR and research into this issue! 💜

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

DST Root CA X3 expiration
3 participants