tls-sslkeylogfile (empty) → 0.1.0.0
raw patch · 5 files changed
+430/−0 lines, 5 filesdep +basedep +bytestringdep +crypton-connection
Dependencies added: base, bytestring, crypton-connection, crypton-x509-system, data-default, http-client, http-client-tls, tls, tls-sslkeylogfile
Files
- CHANGELOG.md +9/−0
- README.md +99/−0
- examples/demo/Demo.hs +14/−0
- src/Network/TLS/SSLKeyLogFile.hs +165/−0
- tls-sslkeylogfile.cabal +143/−0
+ CHANGELOG.md view
@@ -0,0 +1,9 @@+<!--+SPDX-FileCopyrightText: 2025 Mercury Technologies, Inc++SPDX-License-Identifier: MIT+-->++## 0.1.0.0 (2025-03-07)++- Initial release
+ README.md view
@@ -0,0 +1,99 @@+<!--+SPDX-FileCopyrightText: 2025 Mercury Technologies, Inc++SPDX-License-Identifier: MIT+-->++# tls-sslkeylogfile++This package adds support for the [`SSLKEYLOGFILE` standard](https://www.ietf.org/archive/id/draft-thomson-tls-keylogfile-00.html) to Haskell programs using the `tls` and/or `http-client` libraries.++This packages uses the functionality built into the Haskell `tls` library to extract the *session keys* of TLS sessions and store them in the `SSLKEYLOGFILE` format.+In TLS, the session keys are ephemeral *symmetric* keys used to encrypt just the data for each session.+They are generated and exchanged during the TLS handshake, and with modern TLS cipher suites with [Perfect Forward Secrecy], are handled via completely separate machinery (Diffie-Hellman) from the server's private key, the latter only being used to sign the handshake.+As far as key material is concerned, session keys are not very sensitive: they are generated for every session and grabbing them for debugging only allows access to the traffic for which the keys are known.++[Perfect Forward Secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy++Using session keys allows for (relatively) easy debugging of HTTPS traffic using commonly-available packet sniffers like Wireshark *without altering the traffic at all*, and without wider security impacts like e.g. adding local TLS certification authorities to the system keyring.++For more details on TLS interception using session keys, see: <https://jade.fyi/blog/announcing-clipper/>.++## Related work++- [mitmproxy](https://mitmproxy.org/) - an intercepting TLS proxy with a lot of features.++ It can operate as a proxy with fake certificates in various modes including capturing raw traffic and tampering with it, acting as a traditional HTTPS proxy, and more.+ It's really cool.++ However, the fake certificates mode can be unfortunate since they require certificate configuration in the application and are not transparent: you can't use mitmproxy to debug TLS implementation bugs, for instance, since it changes the traffic to intercept it.+- [clipper](https://github.com/lf-/clipper) - a fully integrated TLS session-key-log based packet sniffer for Linux by the same author as this package.++ Clipper does the same thing as this package for rustls and OpenSSL with no application-level changes, implemented by injecting code into the process.+ It captures traffic transparently and can either generate pcapng files with included keys or decode traffic on-the-fly to display it in Chrome DevTools.++ However, it does not support either extracting keys from Haskell or capturing traffic on macOS.++## Usage++Set up tls-sslkeylogfile on a simple program (see [examples/demo/Demo.hs](./examples/demo/Demo.hs)):++```haskell+import Network.TLS.SSLKeyLogFile+import Network.HTTP.Client (parseRequest, httpLbs, Response (..))++main :: IO ()+main = do+ man <- makeManager++ req <- parseRequest "https://example.com/index.html"+ resp <- httpLbs req man+ putStrLn $ "The status code was: " ++ show (responseStatus resp)+```++Then run it with some interception:++In one terminal, capture some packets (tcpdump can equally be used):++```+$ tshark -w pakits.pcapng -i en9 'port 443'+Capturing on 'REDACTED: en9'+322 ^C+```++In another, run the program with `SSLKEYLOGFILE` set:++```+$ SSLKEYLOGFILE=keys.log cabal run keylogfile-demo+The status code was: Status {statusCode = 200, statusMessage = "OK"}+```++After the debugee finishes, the `tshark` command should be CTRL-C'd.++`keys.log` will look like the following:++```+SERVER_HANDSHAKE_TRAFFIC_SECRET aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb+CLIENT_HANDSHAKE_TRAFFIC_SECRET aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc+SERVER_TRAFFIC_SECRET_0 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd+CLIENT_TRAFFIC_SECRET_0 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee+```++The actual keys are replaced with placeholders here for readability.+This is a TLS 1.3 session because there are lines that are not tagged `CLIENT_RANDOM`.++Put the keys into the pcapng so the analysis tools work nicely:++```+$ editcap --inject-secrets tls,keys.log pakits.pcapng pakits2.pcapng+```++Then, look at the packets with tshark:++```+$ tshark -r pakits2.pcapng -T fields -e '_ws.col.Info' --display-filter http+GET /index.html HTTP/1.1+HTTP/1.1 200 OK (text/html)+```++Here's our traffic! If you wanted a nicer UX of looking at it, just open the pcapng file in Wireshark, the encrypted traffic will be right there decrypted for you.
+ examples/demo/Demo.hs view
@@ -0,0 +1,14 @@+-- SPDX-FileCopyrightText: 2025 Mercury Technologies, Inc+--+-- SPDX-License-Identifier: MIT++import Network.HTTP.Client (Response (..), httpLbs, parseRequest)+import Network.TLS.SSLKeyLogFile++main :: IO ()+main = do+ man <- makeManager++ req <- parseRequest "https://example.com/index.html"+ resp <- httpLbs req man+ putStrLn $ "The status code was: " ++ show (responseStatus resp)
+ src/Network/TLS/SSLKeyLogFile.hs view
@@ -0,0 +1,165 @@+-- SPDX-FileCopyrightText: 2025 Mercury Technologies, Inc+--+-- SPDX-License-Identifier: MIT++-- | Implements @SSLKEYLOGFILE@ support: logs keys to the file specified in the+-- @SSLKEYLOGFILE@ environment variable. Completely inactive when the+-- environment variable is not set.+--+-- This is used for inspecting TLS traffic in-flight in combination with a+-- packet dump, with a primary use case of debugging.+--+-- For easier usage, you can embed the key log in the pcap and Wireshark will+-- decrypt it without having to fiddle with any settings:+--+-- > $ editcap --inject-secrets tls,mykeylog.tls_keys input.pcap output.pcapng+--+-- - See: <https://www.ietf.org/archive/id/draft-thomson-tls-keylogfile-00.html>+-- - See: <https://jade.fyi/blog/announcing-clipper/>+module Network.TLS.SSLKeyLogFile (+ -- * Demo and workflow++ -- | There is a demonstration of the workflow of using tls-sslkeylogfile in+ -- the [README on GitHub](https://github.com/MercuryTechnologies/tls-sslkeylogfile#readme)+ -- including all the steps necessary to capture packets and analyze their+ -- decrypted versions.++ -- * Security considerations++ -- | See the RFC: <https://www.ietf.org/archive/id/draft-thomson-tls-keylogfile-00.html#name-security-considerations-8>+ --+ -- This package should be thought of the same as @gdb@ in terms of security. If+ -- the person invoking your software is passing @SSLKEYLOGFILE@, they could+ -- have just as well run it under @gdb@ and gotten the keys out of it without+ -- this package.+ --+ -- The combination of keys extracted from this and traffic decrypted using+ -- those keys, in the absence of session resumption (off by default in hs-tls),+ -- only impacts encryption sessions that were executed while keys were being+ -- logged and no past or future sessions. This assumes that obscure+ -- features like exporters (not supported by hs-tls anyhow as of+ -- 2025-03-06) are not used in a manner that impacts confidentiality of+ -- past or future sessions.+ --+ -- Logging the session keys for debugging has no impact on the security+ -- properties or behaviour of TLS as visible to other hosts; it just allows you+ -- to decrypt traffic as someone who already has full access to the process.++ -- * Functions+ makeManager,+ MakeTLSSettingsParams (..),+ makeTLSSettingsWithKeyLogging,+ addKeyLoggingToClientParams,+) where++import Control.Monad.IO.Class (MonadIO (..))+import Data.Default (Default (..))+import Network.Connection (TLSSettings (..))+import Network.HTTP.Client (Manager, newManager)+import Network.HTTP.Client.TLS (mkManagerSettings)+import Network.TLS (ClientParams (..), DebugParams (..), Shared (..), Supported (..), defaultParamsClient)+import Network.TLS qualified as TLS+import Network.TLS.Extra.Cipher qualified as TLS+import System.Environment (lookupEnv)+import System.IO (IOMode (..), hFlush, hPutStrLn, openFile)+import System.X509 (getSystemCertificateStore)++-- | This is the equivalent of 'TLSSettingsSimple' with only the actually-used+-- fields implemented.+data MakeTLSSettingsParams = MakeTLSSettingsParams+ { disableCertificateValidation :: Bool+ -- ^ Whether to ignore server certificates and not validate them. This is+ -- obviously insecure to set to 'True'.+ --+ -- __Default__: 'False'.+ , clientSupported :: Supported+ -- ^ Value of 'Supported' to use when constructing the 'ClientParams'.+ -- Used for e.g. allowing legacy TLS 1.2 services without Extended Main+ -- Secret support in versions of the Haskell @tls@ library >= 2.0.+ --+ -- __Default__: @def \@Supported {supportedCiphers = TLS.ciphersuite_default}@+ }+ deriving stock (Show)++instance Default MakeTLSSettingsParams where+ def =+ MakeTLSSettingsParams+ { disableCertificateValidation = False+ , clientSupported = def {supportedCiphers = TLS.ciphersuite_default}+ }++-- | Creates a 'TLSSettings' with the system CA trust store and the default+-- cipher suites. This function is for those who want a 'TLSSettings' value that+-- works in normal use cases.+--+-- See @Network.Connection.makeTLSParams@ in @crypton-connection@ for the code+-- this function replicates.+makeTLSSettingsWithKeyLogging :: MakeTLSSettingsParams -> IO TLSSettings+makeTLSSettingsWithKeyLogging MakeTLSSettingsParams {disableCertificateValidation, clientSupported} = do+ caStore <- getSystemCertificateStore++ TLSSettings+ <$> addKeyLoggingToClientParams+ ( (defaultParamsClient "" "")+ { clientShared =+ (def @Shared)+ { sharedCAStore = caStore+ , sharedValidationCache = validationCache+ }+ , TLS.clientSupported+ }+ )+ where+ validationCache+ | disableCertificateValidation =+ TLS.ValidationCache+ (\_ _ _ -> return TLS.ValidationCachePass)+ (\_ _ _ -> return ())+ | otherwise = def++-- | Creates a 'Manager' with TLS support, with the default configuration, with+-- key logging available.+makeManager :: IO Manager+makeManager = do+ tlsSettings <- makeTLSSettingsWithKeyLogging def+ newManager $ mkManagerSettings tlsSettings Nothing++-- | Adds key logging support to the given ClientParams using the+-- @SSLKEYLOGFILE@ environment variable. This is an advanced function. Most use+-- cases should use 'makeTLSSettings' or 'makeManager' to create a+-- 'TLSSettings' or a 'Manager' which works.+--+-- __IMPORTANT NOTE__: If you haven't put a 'sharedCAStore' or 'supportedCiphers'+-- into the given ClientParams, it will not make a connection successfully!+--+-- By default, @crypton-connection@ uses a 'TLSSettingsSimple' into which the CA+-- store and cipher suites are injected during connection setup to make a+-- 'TLSSettings' containing an appropriate 'ClientParams' that can actually+-- establish a TLS connection. By using this function, you are necessarily+-- bypassing this logic and constructing your /own/ 'ClientParams', which,+-- unless you specifically add some, will not have any trusted certification+-- authorities or allowed cipher suites and will thus fail to establish any+-- connection.+addKeyLoggingToClientParams :: ClientParams -> IO ClientParams+addKeyLoggingToClientParams params = do+ keyLogFileEnv <- lookupEnv "SSLKEYLOGFILE"++ -- XXX(jade): due to API design limitations in hs-tls, there's no way to have+ -- a properly managed lifetime for the file handle. we do just leak it. it's+ -- fine, it either gets closed or not (in which case it gets closed on process+ -- exit), but it won't get eaten by the GC or anything either way.++ keyLog <- case keyLogFileEnv of+ Just f -> do+ hand <- openFile f AppendMode+ pure $ \line -> liftIO (hPutStrLn hand line >> hFlush hand)+ Nothing -> do+ pure $ \_ -> pure ()++ pure+ params+ { clientDebug =+ (def @DebugParams)+ { debugKeyLogger = keyLog+ }+ }
+ tls-sslkeylogfile.cabal view
@@ -0,0 +1,143 @@+cabal-version: 3.0++-- This file has been generated from package.yaml by hpack version 0.36.1.+--+-- see: https://github.com/sol/hpack++name: tls-sslkeylogfile+version: 0.1.0.0+synopsis: SSLKEYLOGFILE support for Haskell+description: See README at <https://github.com/MercuryTechnologies/tls-sslkeylogfile#readme>.+category: Network+homepage: https://github.com/MercuryTechnologies/tls-sslkeylogfile#readme+bug-reports: https://github.com/MercuryTechnologies/tls-sslkeylogfile/issues+maintainer: Jade Lovelace <jadel@mercury.com>+license: MIT+build-type: Simple+tested-with:+ GHC == { 9.2, 9.4, 9.6, 9.8, 9.10 }+extra-source-files:+ CHANGELOG.md+ README.md++source-repository head+ type: git+ location: https://github.com/MercuryTechnologies/tls-sslkeylogfile++flag examples+ description: Build example executables.+ manual: True+ default: False++library+ exposed-modules:+ Network.TLS.SSLKeyLogFile+ other-modules:+ Paths_tls_sslkeylogfile+ autogen-modules:+ Paths_tls_sslkeylogfile+ hs-source-dirs:+ src+ default-extensions:+ AllowAmbiguousTypes+ BlockArguments+ DataKinds+ DeriveAnyClass+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveLift+ DeriveTraversable+ DerivingVia+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ GADTs+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ InstanceSigs+ LambdaCase+ MonoLocalBinds+ MultiWayIf+ NamedFieldPuns+ NumericUnderscores+ OverloadedStrings+ PatternSynonyms+ PolyKinds+ RankNTypes+ RecordWildCards+ RecursiveDo+ ScopedTypeVariables+ StandaloneDeriving+ StandaloneKindSignatures+ TypeApplications+ TypeFamilies+ ViewPatterns+ ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-export-lists -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-missing-kind-signatures -Wno-implicit-prelude+ build-depends:+ base >=4.16 && <4.22+ , crypton-connection+ , crypton-x509-system+ , data-default+ , http-client+ , http-client-tls+ , tls >=1.8 && <2.0 || >=2.0 && <2.3+ default-language: Haskell2010++executable keylogfile-demo+ main-is: Demo.hs+ other-modules:+ Paths_tls_sslkeylogfile+ autogen-modules:+ Paths_tls_sslkeylogfile+ hs-source-dirs:+ examples/demo+ default-extensions:+ AllowAmbiguousTypes+ BlockArguments+ DataKinds+ DeriveAnyClass+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveLift+ DeriveTraversable+ DerivingVia+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ GADTs+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ InstanceSigs+ LambdaCase+ MonoLocalBinds+ MultiWayIf+ NamedFieldPuns+ NumericUnderscores+ OverloadedStrings+ PatternSynonyms+ PolyKinds+ RankNTypes+ RecordWildCards+ RecursiveDo+ ScopedTypeVariables+ StandaloneDeriving+ StandaloneKindSignatures+ TypeApplications+ TypeFamilies+ ViewPatterns+ ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-export-lists -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-missing-kind-signatures -Wno-implicit-prelude+ build-depends:+ base >=4.16 && <4.22+ , bytestring+ , crypton-connection+ , crypton-x509-system+ , data-default+ , http-client+ , http-client-tls+ , tls >=1.8 && <2.0 || >=2.0 && <2.3+ , tls-sslkeylogfile+ default-language: Haskell2010+ if !flag(examples)+ buildable: False