From b859de8a694c5e6506d6c4ee08948daf6c7eb6f8 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 4 Nov 2025 21:21:22 +0100 Subject: [PATCH 1/4] Fix compilation for client lib --- simplexmq.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplexmq.cabal b/simplexmq.cabal index 57ceaa599..d72d3f02c 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -312,6 +312,7 @@ library , directory ==1.3.* , filepath ==1.4.* , hourglass ==0.2.* + , http-client ==0.7.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , iproute ==1.7.* @@ -343,7 +344,6 @@ library case-insensitive ==1.2.* , hashable ==1.4.* , ini ==0.4.1 - , http-client ==0.7.* , http-client-tls ==0.3.6.* , optparse-applicative >=0.15 && <0.17 , process ==1.6.* From 648f37d0eb26d1041d33ba9ccb4b232b3921c4f1 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 4 Nov 2025 21:21:35 +0100 Subject: [PATCH 2/4] Print VAPID fp --- src/Simplex/Messaging/Notifications/Server/Main.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index db2279e64..21e31f00f 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -57,7 +57,7 @@ import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) import Text.Read (readMaybe) import System.Process (readCreateProcess, shell) -import Simplex.Messaging.Notifications.Server.Push.WebPush (WebPushConfig(..), VapidKey, mkVapid) +import Simplex.Messaging.Notifications.Server.Push.WebPush (WebPushConfig(..), VapidKey(..), mkVapid) ntfServerCLI :: FilePath -> FilePath -> IO () ntfServerCLI cfgPath logPath = @@ -215,12 +215,13 @@ ntfServerCLI cfgPath logPath = hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering fp <- checkSavedFingerprint cfgPath defaultX509Config - vapidKey <- getVapidKey vapidKeyPath + vapidKey@VapidKey {fp = vapidFp } <- getVapidKey vapidKeyPath let host = either (const "") T.unpack $ lookupValue "TRANSPORT" "host" ini port = T.unpack $ strictIni "TRANSPORT" "port" ini cfg@NtfServerConfig {transports} = serverConfig vapidKey srv = ProtoServerWithAuth (NtfServer [THDomainName host] (if port == "443" then "" else port) (C.KeyHash fp)) Nothing printServiceInfo serverVersion srv + B.putStrLn $ "VAPID: " <> vapidFp printNtfServerConfig transports dbStoreConfig runNtfServer cfg where From 3179d8089ef731ad72bed097a05758b56aec07c1 Mon Sep 17 00:00:00 2001 From: sim Date: Wed, 5 Nov 2025 12:01:03 +0100 Subject: [PATCH 3/4] Fix VAPID signature --- src/Simplex/Messaging/Crypto.hs | 1 + src/Simplex/Messaging/Notifications/Server/Push.hs | 11 +++++++++++ .../Messaging/Notifications/Server/Push/WebPush.hs | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index 05c6b32e9..bf2a4ac3b 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -95,6 +95,7 @@ module Simplex.Messaging.Crypto encodePrivKey, decodePrivKey, pubKeyBytes, + encodeBigInt, uncompressEncodePoint, uncompressDecodePoint, uncompressDecodePrivateNumber, diff --git a/src/Simplex/Messaging/Notifications/Server/Push.hs b/src/Simplex/Messaging/Notifications/Server/Push.hs index 1039e5448..296b686d3 100644 --- a/src/Simplex/Messaging/Notifications/Server/Push.hs +++ b/src/Simplex/Messaging/Notifications/Server/Push.hs @@ -89,6 +89,17 @@ signedJWTToken pk (JWTToken hdr claims) = do jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode serialize sig = U.encodeUnpadded $ encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence] +-- | Does it work with APNS ? +signedJWTTokenRawSign :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken +signedJWTTokenRawSign pk (JWTToken hdr claims) = do + let hc = jwtEncode hdr <> "." <> jwtEncode claims + sig <- EC.sign pk SHA256 hc + pure $ hc <> "." <> serialize sig + where + jwtEncode :: ToJSON a => a -> ByteString + jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode + serialize sig = U.encodeUnpadded $ LB.toStrict $ C.encodeBigInt (EC.sign_r sig) <> C.encodeBigInt (EC.sign_s sig) + readECPrivateKey :: FilePath -> IO EC.PrivateKey readECPrivateKey f = do -- this pattern match is specific to APNS key type, it may need to be extended for other push providers diff --git a/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs b/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs index 3de34cb9c..d8f2de142 100644 --- a/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs +++ b/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs @@ -125,11 +125,11 @@ mkVapidHeader VapidKey {key, fp} uriAuthority expire = do { iss = Nothing, iat = Nothing, exp = Just expire, - aud = Just $ T.decodeUtf8 uriAuthority, + aud = Just $ T.decodeUtf8 $ "https://" <> uriAuthority, sub = Just "https://github.com/simplex-chat/simplexmq/" } jwt = JWTToken jwtHeader jwtClaims - signedToken <- signedJWTToken key jwt + signedToken <- signedJWTTokenRawSign key jwt pure $ "vapid t=" <> signedToken <> ",k=" <> fp wpPushProviderClient :: WebPushClient -> PushProviderClient From ef092351c8d9f145e7654cf227d37ec1f61f8173 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 19 Jan 2026 20:48:01 +0000 Subject: [PATCH 4/4] refactor --- .../Messaging/Notifications/Server/Main.hs | 25 +++++---- .../Messaging/Notifications/Server/Push.hs | 52 +++++++++---------- .../Notifications/Server/Push/WebPush.hs | 2 +- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index 21e31f00f..d2c2d393b 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -11,7 +11,7 @@ module Simplex.Messaging.Notifications.Server.Main where import Control.Logger.Simple (setLogLevel) -import Control.Monad ( (<$!>), unless, void ) +import Control.Monad (unless, void, (<$!>)) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) @@ -31,9 +31,10 @@ import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClie import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol (NtfTokenId) -import Simplex.Messaging.Notifications.Server (runNtfServer, restoreServerLastNtfs) +import Simplex.Messaging.Notifications.Server (restoreServerLastNtfs, runNtfServer) import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration) import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig) +import Simplex.Messaging.Notifications.Server.Push.WebPush (VapidKey (..), WebPushConfig (..), mkVapid) import Simplex.Messaging.Notifications.Server.Store (newNtfSTMStore) import Simplex.Messaging.Notifications.Server.Store.Postgres (exportNtfDbStore, importNtfSTMStore, newNtfDbStore) import Simplex.Messaging.Notifications.Server.StoreLog (readWriteNtfSTMStore) @@ -55,9 +56,8 @@ import System.Directory (createDirectoryIfMissing, doesFileExist, renameFile) import System.Exit (exitFailure) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) -import Text.Read (readMaybe) import System.Process (readCreateProcess, shell) -import Simplex.Messaging.Notifications.Server.Push.WebPush (WebPushConfig(..), VapidKey(..), mkVapid) +import Text.Read (readMaybe) ntfServerCLI :: FilePath -> FilePath -> IO () ntfServerCLI cfgPath logPath = @@ -215,7 +215,7 @@ ntfServerCLI cfgPath logPath = hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering fp <- checkSavedFingerprint cfgPath defaultX509Config - vapidKey@VapidKey {fp = vapidFp } <- getVapidKey vapidKeyPath + vapidKey@VapidKey {fp = vapidFp} <- getVapidKey vapidKeyPath let host = either (const "") T.unpack $ lookupValue "TRANSPORT" "host" ini port = T.unpack $ strictIni "TRANSPORT" "port" ini cfg@NtfServerConfig {transports} = serverConfig vapidKey @@ -361,18 +361,21 @@ cliCommandP cfgPath logPath iniFile = skipTokensP = option strParse - ( long "skip-tokens" - <> help "Skip tokens during import" - <> value S.empty - ) + ( long "skip-tokens" + <> help "Skip tokens during import" + <> value S.empty + ) initP :: Parser InitOptions initP = do enableStoreLog <- - flag' False + flag' + False ( long "disable-store-log" <> help "Disable store log for persistence (enabled by default)" ) - <|> flag True True + <|> flag + True + True ( long "store-log" <> short 'l' <> help "Enable store log for persistence (DEPRECATED, enabled by default)" diff --git a/src/Simplex/Messaging/Notifications/Server/Push.hs b/src/Simplex/Messaging/Notifications/Server/Push.hs index 296b686d3..ff21de2d4 100644 --- a/src/Simplex/Messaging/Notifications/Server/Push.hs +++ b/src/Simplex/Messaging/Notifications/Server/Push.hs @@ -10,6 +10,8 @@ module Simplex.Messaging.Notifications.Server.Push where +import Control.Exception (Exception) +import Control.Monad.Except (ExceptT) import Crypto.Hash.Algorithms (SHA256 (..)) import qualified Crypto.PubKey.ECC.ECDSA as EC import qualified Crypto.PubKey.ECC.Types as ECT @@ -28,15 +30,13 @@ import Data.List.NonEmpty (NonEmpty (..)) import Data.Text (Text) import Data.Time.Clock.System import qualified Data.X509 as X +import GHC.Exception (SomeException) +import Network.HTTP.Types (Status) +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol +import Simplex.Messaging.Notifications.Server.Store.Types (NtfTknRec) import Simplex.Messaging.Parsers (defaultJSON) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2ClientError) -import qualified Simplex.Messaging.Crypto as C -import Network.HTTP.Types (Status) -import Control.Exception (Exception) -import Simplex.Messaging.Notifications.Server.Store.Types (NtfTknRec) -import Control.Monad.Except (ExceptT) -import GHC.Exception (SomeException) data JWTHeader = JWTHeader { typ :: Text, -- "JWT" @@ -46,7 +46,7 @@ data JWTHeader = JWTHeader deriving (Show) mkJWTHeader :: Text -> Maybe Text -> JWTHeader -mkJWTHeader alg kid = JWTHeader { typ = "JWT", alg, kid } +mkJWTHeader alg kid = JWTHeader {typ = "JWT", alg, kid} data JWTClaims = JWTClaims { iss :: Maybe Text, -- issuer, team ID for APNS @@ -65,13 +65,14 @@ mkJWTToken hdr iss = do iat <- systemSeconds <$> getSystemTime pure $ JWTToken hdr $ jwtClaims iat where - jwtClaims iat = JWTClaims - { iss = Just iss, - iat = Just iat, - exp = Nothing, - aud = Nothing, - sub = Nothing - } + jwtClaims iat = + JWTClaims + { iss = Just iss, + iat = Just iat, + exp = Nothing, + aud = Nothing, + sub = Nothing + } type SignedJWTToken = ByteString @@ -79,26 +80,23 @@ $(JQ.deriveToJSON defaultJSON ''JWTHeader) $(JQ.deriveToJSON defaultJSON ''JWTClaims) -signedJWTToken :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken -signedJWTToken pk (JWTToken hdr claims) = do +signedJWTToken_ :: (EC.Signature -> ByteString) -> EC.PrivateKey -> JWTToken -> IO SignedJWTToken +signedJWTToken_ serialize pk (JWTToken hdr claims) = do let hc = jwtEncode hdr <> "." <> jwtEncode claims sig <- EC.sign pk SHA256 hc - pure $ hc <> "." <> serialize sig + pure $ hc <> "." <> U.encodeUnpadded (serialize sig) where jwtEncode :: ToJSON a => a -> ByteString jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode - serialize sig = U.encodeUnpadded $ encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence] + +signedJWTToken :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken +signedJWTToken = signedJWTToken_ $ \sig -> + encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence] -- | Does it work with APNS ? -signedJWTTokenRawSign :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken -signedJWTTokenRawSign pk (JWTToken hdr claims) = do - let hc = jwtEncode hdr <> "." <> jwtEncode claims - sig <- EC.sign pk SHA256 hc - pure $ hc <> "." <> serialize sig - where - jwtEncode :: ToJSON a => a -> ByteString - jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode - serialize sig = U.encodeUnpadded $ LB.toStrict $ C.encodeBigInt (EC.sign_r sig) <> C.encodeBigInt (EC.sign_s sig) +signedJWTTokenRaw :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken +signedJWTTokenRaw = signedJWTToken_ $ \sig -> + C.encodeBigInt (EC.sign_r sig) <> C.encodeBigInt (EC.sign_s sig) readECPrivateKey :: FilePath -> IO EC.PrivateKey readECPrivateKey f = do diff --git a/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs b/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs index d8f2de142..d6a656d86 100644 --- a/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs +++ b/src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs @@ -129,7 +129,7 @@ mkVapidHeader VapidKey {key, fp} uriAuthority expire = do sub = Just "https://github.com/simplex-chat/simplexmq/" } jwt = JWTToken jwtHeader jwtClaims - signedToken <- signedJWTTokenRawSign key jwt + signedToken <- signedJWTTokenRaw key jwt pure $ "vapid t=" <> signedToken <> ",k=" <> fp wpPushProviderClient :: WebPushClient -> PushProviderClient