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

Add the ability to load TLS credentials from IORefs #806

Merged
merged 8 commits into from
Jun 25, 2020
Merged

Add the ability to load TLS credentials from IORefs #806

merged 8 commits into from
Jun 25, 2020

Conversation

mbg
Copy link
Contributor

@mbg mbg commented Jun 22, 2020

This PR adds three new configuration fields to TLSSettings for retrieving the certificate bytes, chain certificate bytes, and key bytes from IORefs. This is potentially useful for scenarios where the certificate may need to change at runtime, e.g. if ACME is used. In theory, this would prevent an ACME implementation from having to terminate the server thread in order to call runTLS with new TLS credentials. However, there is currently no ACME implementation in Haskell (to my knowledge), so this is a somewhat speculative PR.

Copy link
Member

@snoyberg snoyberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kazu-yamamoto do you have any issues with the changes here?

@mbg
Copy link
Contributor Author

mbg commented Jun 23, 2020

I can bump the version to 3.3.0 in light of the export changes if everything else is OK.

@kazu-yamamoto kazu-yamamoto self-assigned this Jun 23, 2020
@kazu-yamamoto kazu-yamamoto self-requested a review June 23, 2020 22:06
@kazu-yamamoto
Copy link
Contributor

LGTM.

@snoyberg Should we bump the version to 3.3.0?

@kazu-yamamoto
Copy link
Contributor

This should close #463

@snoyberg
Copy link
Member

Yes, let's go for the 3.3.0 version bump.

@mbg
Copy link
Contributor Author

mbg commented Jun 24, 2020

Done.

Copy link
Member

@snoyberg snoyberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks! @kazu-yamamoto want me to merge and release?

@kazu-yamamoto
Copy link
Contributor

I would like to test this code for a while. After that I will take care of merging and releasing.

Copy link
Contributor

@kazu-yamamoto kazu-yamamoto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kazu-yamamoto kazu-yamamoto merged commit 4974a41 into yesodweb:master Jun 25, 2020
@kazu-yamamoto
Copy link
Contributor

v3.3.0 has been released. Thank you for your contribution!

Copy link

@vdukhovni vdukhovni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the results of this merged PR are not getting used much yet, perhaps it might not be too late to make post-release changes?

Comment on lines +76 to +79
data CertSettings
= CertFromFile !FilePath ![FilePath] !FilePath
| CertFromMemory !S.ByteString ![S.ByteString] !S.ByteString
| CertFromRef !(I.IORef S.ByteString) ![I.IORef S.ByteString] !(I.IORef S.ByteString)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm rather late to party, sorry about that, just noticed this PR today, and sadly it has been merged for some time now. And yet perhaps I should make my comments anyway. My first issue is with the choice of separate IORefs for each of the cert, chain and keys. I think that's likely a mistake. Rather I think that the IORef representation should have been:

CertFromRef !(I.IORef (S.ByteString,  [S.ByteString], S.ByteString))

That is, a single IORef holding a triple with the certificate, issuer chain, and key. This would allow atomic retrieval of the triple, in which one can be confident that all three objects correspond to each other. The current design is subject race conditions between reader and writer, because the writer updates multiple IORefs separately, and the reader may see an inconsistent view of the data. :-(

Comment on lines +270 to +273
tlsSettingsRef
:: I.IORef S.ByteString -- ^ Reference to certificate bytes
-> I.IORef (S.ByteString) -- ^ Reference to key bytes
-> TLSSettings

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have argued strongly against providing this function. The most widely used CA on the planet is now "Let's Encrypt" and it along with almost all the other CAs does not sign the leaf certificate directly with a trusted root CA. Instead pretty much all certificates that chain up to one of the usual WebPKI CAs are signed by an intermediate CA.

When you have a certificate signed by an intermediate CA, while various browsers are willing to pay the cost of dynamically locating intermediate CA certs from various certificate extensions, and may have an intermediate cert cache, other software tools (e.g. the ubiquitous curl, etc.) are not, and the TLS specifications DO NOT make intermediate certificates optional. They are a required part of the server's certificate chain.

Here the caller can always provide an empty chain if that's what they really want to do, but there's no need to encourage such negligence by providing an API entry point that can only do the wrong thing. The more general entry point below is the only one that should have been offered.

Comment on lines +282 to +286
tlsSettingsChainRef
:: I.IORef S.ByteString -- ^ Reference to certificate bytes
-> [I.IORef S.ByteString] -- ^ Reference to chain certificate bytes
-> I.IORef (S.ByteString) -- ^ Reference to key bytes
-> TLSSettings

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And of course in this function, there should have been just a single IORef holding a triple of (Cert, [Chain], Key) bytestrings/lists of bytestrings.

Comment on lines +309 to +314
CertFromRef certRef chainCertsRef keyRef -> do
cert <- I.readIORef certRef
chainCerts <- mapM I.readIORef chainCertsRef
key <- I.readIORef keyRef
cred <- either error return $ TLS.credentialLoadX509ChainFromMemory cert chainCerts key
return $ TLS.Credentials [cred]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which brings us to perhaps the most important issue. What is the purpose of the IORef alternative, if ultimately loadCredentials converts them (if I'm not mistaken just once) immediately to a set of pure objects used in:

runTLSSocket :: TLSSettings -> Settings -> Socket -> Application -> IO ()
runTLSSocket tlsset set sock app = do
    credentials <- loadCredentials tlsset
    mgr <- getSessionManager tlsset
    runTLSSocket' tlsset set credentials mgr sock app

This still means that the application loop needs to be restarted when new certificates are available, and so I fail to see the difference between this CertFromMemory. What does the indirection via IORefs achieve?

It seems like there's much missing code here to make a practically useful deployment model. Perhaps the idea is to also provide a restart loop (I may start work on that shortly) and have the restart loop be configured once with these (or ideally just one) IORefs, allowing another thread to just worry about updating the data as needed?

If so I think that code should probably have appeared concurrently with or even before this PR, which would have made it easier to see whether this API is the right one for the job.

[ My tentative loop restart implementation idea will only work on POSIX systems that support dup(2) on socket file descriptors. Which precludes Windows. I hope that having reasonably clean certificate reload without dropping any connections even briefly, but just on Unix-like systems is an acceptable design choice. More ambitious designs would likely require invasive changes in the TLS library, which are perhaps a good idea, but I'm not planning to go there at this time. ]

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.

4 participants