Files
simplexmq/src/Simplex/RemoteControl/Invitation.hs
Evgeny Poberezkin dd2bd11584 parameterize version scopes with phantom types (#1026)
* parameterize version scopes with phantom types

* move Version to another module

* parens
2024-03-04 19:06:51 +00:00

170 lines
5.8 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.RemoteControl.Invitation
( RCInvitation (..)
, signInvitation
, RCSignedInvitation (..)
, verifySignedInvitation
, RCVerifiedInvitation (..)
, RCEncInvitation (..)
) where
import qualified Data.Aeson as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB
import Data.Time.Clock.System (SystemTime)
import Data.Word (Word16)
import Network.HTTP.Types (parseSimpleQuery)
import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery, urlDecode)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.RemoteControl.Types (VersionRangeRCP)
data RCInvitation = RCInvitation
{ -- | CA TLS certificate fingerprint of the controller.
--
-- This is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
ca :: C.KeyHash,
host :: TransportHost,
port :: Word16,
-- | Supported version range for remote control protocol
v :: VersionRangeRCP,
-- | Application information
app :: J.Value,
-- | Session start time in seconds since epoch
ts :: SystemTime,
-- | Session Ed25519 public key used to verify the announcement and commands
--
-- This mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
skey :: C.PublicKeyEd25519,
-- | Long-term Ed25519 public key used to verify the announcement and commands.
--
-- Is apart of the long term controller identity.
idkey :: C.PublicKeyEd25519,
-- | Session X25519 DH key
dh :: C.PublicKeyX25519
}
deriving (Show)
instance StrEncoding RCInvitation where
strEncode RCInvitation {ca, host, port, v, app, ts, skey, idkey, dh} =
mconcat
[ "xrcp:/",
strEncode ca,
"@",
strEncode host,
":",
strEncode port,
"#/?",
renderSimpleQuery False query
]
where
query =
[ ("v", strEncode v),
("app", LB.toStrict $ J.encode app),
("ts", strEncode ts),
("skey", strEncode skey),
("idkey", strEncode idkey),
("dh", strEncode dh)
]
strP = do
_ <- A.string "xrcp:/"
ca <- strP
_ <- A.char '@'
host <- A.takeWhile (/= ':') >>= either fail pure . strDecode . urlDecode True
_ <- A.char ':'
port <- strP
_ <- A.string "#/?"
q <- parseSimpleQuery <$> A.takeWhile (/= ' ')
v <- requiredP q "v" strDecode
app <- requiredP q "app" $ J.eitherDecodeStrict . urlDecode True
ts <- requiredP q "ts" $ strDecode . urlDecode True
skey <- requiredP q "skey" $ parseAll strP
idkey <- requiredP q "idkey" $ parseAll strP
dh <- requiredP q "dh" $ parseAll strP
pure RCInvitation {ca, host, port, v, app, ts, skey, idkey, dh}
data RCSignedInvitation = RCSignedInvitation
{ invitation :: RCInvitation,
ssig :: C.Signature 'C.Ed25519,
idsig :: C.Signature 'C.Ed25519
}
deriving (Show)
-- | URL-encoded and signed for showing in QR code
instance StrEncoding RCSignedInvitation where
strEncode RCSignedInvitation {invitation, ssig, idsig} =
mconcat
[ strEncode invitation,
"&ssig=",
strEncode $ C.signatureBytes ssig,
"&idsig=",
strEncode $ C.signatureBytes idsig
]
strP = do
-- TODO this assumes some order or parameters, can be made independent
(url, invitation) <- A.match strP
sigs <- case B.breakSubstring "&ssig=" url of
(_, sigs) | B.null sigs -> fail "missing signatures"
(_, sigs) -> pure $ parseSimpleQuery $ B.drop 1 sigs
ssig <- requiredP sigs "ssig" $ parseAll strP
idsig <- requiredP sigs "idsig" $ parseAll strP
pure RCSignedInvitation {invitation, ssig, idsig}
signInvitation :: C.PrivateKey C.Ed25519 -> C.PrivateKey C.Ed25519 -> RCInvitation -> RCSignedInvitation
signInvitation sKey idKey invitation = RCSignedInvitation {invitation, ssig, idsig}
where
uri = strEncode invitation
ssig =
case C.sign (C.APrivateSignKey C.SEd25519 sKey) uri of
C.ASignature C.SEd25519 s -> s
_ -> error "signing with ed25519"
inviteUrlSigned = mconcat [uri, "&ssig=", strEncode ssig]
idsig =
case C.sign (C.APrivateSignKey C.SEd25519 idKey) inviteUrlSigned of
C.ASignature C.SEd25519 s -> s
_ -> error "signing with ed25519"
newtype RCVerifiedInvitation = RCVerifiedInvitation RCInvitation
deriving (Show)
verifySignedInvitation :: RCSignedInvitation -> Maybe RCVerifiedInvitation
verifySignedInvitation RCSignedInvitation {invitation, ssig, idsig} =
if C.verify' skey ssig inviteURL && C.verify' idkey idsig inviteURLS
then Just $ RCVerifiedInvitation invitation
else Nothing
where
RCInvitation {skey, idkey} = invitation
inviteURL = strEncode invitation
inviteURLS = mconcat [inviteURL, "&ssig=", strEncode ssig]
data RCEncInvitation = RCEncInvitation
{ dhPubKey :: C.PublicKeyX25519,
nonce :: C.CbNonce,
encInvitation :: ByteString
}
instance Encoding RCEncInvitation where
smpEncode RCEncInvitation {dhPubKey, nonce, encInvitation} =
smpEncode (dhPubKey, nonce, Tail encInvitation)
smpP = do
(dhPubKey, nonce, Tail encInvitation) <- smpP
pure RCEncInvitation {dhPubKey, nonce, encInvitation}
-- * Utils
requiredP :: MonadFail m => SimpleQuery -> ByteString -> (ByteString -> Either String a) -> m a
requiredP q k f = maybe (fail $ "missing " <> show k) (either fail pure . f) $ lookup k q