diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+<!--
+SPDX-FileCopyrightText: 2025 Mercury Technologies, Inc
+
+SPDX-License-Identifier: MIT
+-->
+
+## 0.1.0.0 (2025-03-07)
+
+- Initial release
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/examples/demo/Demo.hs b/examples/demo/Demo.hs
new file mode 100644
--- /dev/null
+++ b/examples/demo/Demo.hs
@@ -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)
diff --git a/src/Network/TLS/SSLKeyLogFile.hs b/src/Network/TLS/SSLKeyLogFile.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/TLS/SSLKeyLogFile.hs
@@ -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
+            }
+      }
diff --git a/tls-sslkeylogfile.cabal b/tls-sslkeylogfile.cabal
new file mode 100644
--- /dev/null
+++ b/tls-sslkeylogfile.cabal
@@ -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
