diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 1b1bb4a5b9..e3d4e72282 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -40,6 +40,7 @@ library Simplex.Chat.AppSettings Simplex.Chat.Badges Simplex.Chat.Badges.CLI + Simplex.Chat.Names Simplex.Chat.Call Simplex.Chat.Controller Simplex.Chat.Delivery diff --git a/src/Simplex/Chat/Badges.hs b/src/Simplex/Chat/Badges.hs index 226a5e5229..c1aa7d53a6 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -30,9 +30,6 @@ module Simplex.Chat.Badges maxFileSizeLegend, ProofPresHeaderTag (..), ProofPresHeader (..), - NameClaimProof (..), - signNameProof, - verifyNameProofSig, proofPresHeaderLink, BadgePurchase (..), BadgeMasterKey (..), @@ -246,35 +243,6 @@ proofPresHeaderAccepted = \case PHSimplexLink _ -> True PHUnknown _ _ -> True --- A name claim proof: signed by the address owner's key (linkOwnerId = Just oid when a channel --- owner other than the address signs, Nothing when the address's own root key signs) over --- strEncode name <> strEncode presHeader, tied to the link it is shown through. -data NameClaimProof = NameClaimProof - { linkOwnerId :: Maybe (StrJSON "OwnerId" OwnerId), - presHeader :: ProofPresHeader, - signature :: C.Signature 'C.Ed25519 - } - deriving (Eq, Show) - -nameProofPayload :: SimplexNameInfo -> ProofPresHeader -> ByteString -nameProofPayload name presHeader = strEncode name <> strEncode presHeader - --- linkOwnerId names the signing owner in the link's owner chain (Nothing = root key for a contact address). -signNameProof :: C.PrivateKeyEd25519 -> Maybe OwnerId -> SimplexNameInfo -> ProofPresHeader -> NameClaimProof -signNameProof key linkOwnerId name presHeader = - NameClaimProof - { linkOwnerId = StrJSON <$> linkOwnerId, - presHeader, - signature = C.sign' key (nameProofPayload name presHeader) - } - --- verify a name proof's signature against the resolved address owner key. The caller must --- SEPARATELY check the proof's presHeader link is the one it was shown through, so a proof made --- for one link can't be reused on another. -verifyNameProofSig :: C.PublicKeyEd25519 -> SimplexNameInfo -> NameClaimProof -> Bool -verifyNameProofSig ownerKey name NameClaimProof {presHeader, signature} = - C.verify' ownerKey signature (nameProofPayload name presHeader) - proofPresHeaderLink :: ProofPresHeader -> Maybe AConnShortLink proofPresHeaderLink = \case PHSimplexLink lnk -> Just lnk @@ -444,13 +412,6 @@ $(JQ.deriveJSON defaultJSON ''BadgeCredential) $(JQ.deriveJSON defaultJSON ''BadgeProof) -$(JQ.deriveJSON defaultJSON ''NameClaimProof) - --- NameClaimProof is stored as JSON in contact_profiles.contact_domain_proof (like a badge proof) -instance ToField NameClaimProof where toField = toField . encodeJSON - -instance FromField NameClaimProof where fromField = fromTextField_ decodeJSON - -- LocalBadge is sent to the UI/clients WITHOUT crypto - only disclosed info + status. The credential/proof -- bytes stay core-side. FromJSON reconstructs a display-only badge (empty proof) for read-only consumers -- (remote host, UI echoes); the authoritative badge is loaded from the DB (rowToBadge), never from this JSON. diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index d0dc917d67..1b33d4a61e 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -55,7 +55,8 @@ import Data.Type.Equality import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Simplex.Chat.Library.Subscriber -import Simplex.Chat.Badges (BadgeCredential (..), NameClaimProof (..), LocalBadge (..), ProofPresHeader (..), maxXFTPFileSize, mkBadgeStatus, proofPresHeaderLink, signNameProof, verifyCredential, verifyNameProofSig) +import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), ProofPresHeader (..), maxXFTPFileSize, mkBadgeStatus, proofPresHeaderLink, verifyCredential) +import Simplex.Chat.Names (NameClaimProof (..), signNameProof, verifyNameProofSig) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..)) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index fc3a8c043f..2ec3250f35 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -53,7 +53,8 @@ import Data.Text.Encoding (encodeUtf8) import Data.Time (addUTCTime) import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds, secondsToDiffTime) -import Simplex.Chat.Badges (BadgeCredential (..), ProofPresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, signNameProof, verifyBadge) +import Simplex.Chat.Badges (BadgeCredential (..), ProofPresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Names (signNameProof) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Files diff --git a/src/Simplex/Chat/Names.hs b/src/Simplex/Chat/Names.hs new file mode 100644 index 0000000000..194e17934e --- /dev/null +++ b/src/Simplex/Chat/Names.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Names + ( NameClaimProof (..), + signNameProof, + verifyNameProofSig, + ) +where + +import qualified Data.Aeson.TH as JQ +import Data.ByteString.Char8 (ByteString) +import Simplex.Chat.Badges (ProofPresHeader) +import Simplex.Messaging.Agent.Protocol (OwnerId) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON) +import Simplex.Messaging.SimplexName (SimplexNameInfo) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple.FromField (FromField (..)) +import Database.PostgreSQL.Simple.ToField (ToField (..)) +#else +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +#endif + +-- A name claim proof: signed by the address owner's key (linkOwnerId = Just oid when a channel +-- owner other than the address signs, Nothing when the address's own root key signs) over +-- strEncode name <> strEncode presHeader, tied to the link it is shown through. +data NameClaimProof = NameClaimProof + { linkOwnerId :: Maybe (StrJSON "OwnerId" OwnerId), + presHeader :: ProofPresHeader, + signature :: C.Signature 'C.Ed25519 + } + deriving (Eq, Show) + +nameProofPayload :: SimplexNameInfo -> ProofPresHeader -> ByteString +nameProofPayload name presHeader = strEncode name <> strEncode presHeader + +-- linkOwnerId names the signing owner in the link's owner chain (Nothing = root key for a contact address). +signNameProof :: C.PrivateKeyEd25519 -> Maybe OwnerId -> SimplexNameInfo -> ProofPresHeader -> NameClaimProof +signNameProof key linkOwnerId name presHeader = + NameClaimProof + { linkOwnerId = StrJSON <$> linkOwnerId, + presHeader, + signature = C.sign' key (nameProofPayload name presHeader) + } + +-- verify a name proof's signature against the resolved address owner key. The caller must +-- SEPARATELY check the proof's presHeader link is the one it was shown through, so a proof made +-- for one link can't be reused on another. +verifyNameProofSig :: C.PublicKeyEd25519 -> SimplexNameInfo -> NameClaimProof -> Bool +verifyNameProofSig ownerKey name NameClaimProof {presHeader, signature} = + C.verify' ownerKey signature (nameProofPayload name presHeader) + +$(JQ.deriveJSON defaultJSON ''NameClaimProof) + +-- stored as JSON in contact_profiles.contact_domain_proof +instance ToField NameClaimProof where toField = toField . encodeJSON + +instance FromField NameClaimProof where fromField = fromTextField_ decodeJSON diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index f139f462ce..0f85421d2f 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -222,7 +222,8 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime) import Data.Text.Encoding (encodeUtf8) -import Simplex.Chat.Badges (BadgeRow, NameClaimProof, badgeToRow, verifyBadge_) +import Simplex.Chat.Badges (BadgeRow, badgeToRow, verifyBadge_) +import Simplex.Chat.Names (NameClaimProof) import Simplex.Chat.Messages import Simplex.Chat.Operators import Simplex.Chat.Protocol hiding (Binary) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index e7ce721b77..7ad9bdcb30 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -33,7 +33,8 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Type.Equality -import Simplex.Chat.Badges (BadgeRow, NameClaimProof, badgeToRow, rowToBadge, verifyBadge_) +import Simplex.Chat.Badges (BadgeRow, badgeToRow, rowToBadge, verifyBadge_) +import Simplex.Chat.Names (NameClaimProof) import Simplex.Chat.Messages import Simplex.Chat.Remote.Types import Simplex.Chat.Types diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 0feec9b588..8a7bcc324e 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -48,7 +48,8 @@ import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime) import Data.Typeable (Typeable) import Data.Word (Word16) -import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), NameClaimProof (..), LocalBadge (..), localBadgeInfo, localBadgeStatus, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), localBadgeInfo, localBadgeStatus, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Names (NameClaimProof (..)) import Simplex.Messaging.Crypto.BBS (BBSPublicKey) import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared