mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 20:45:52 +00:00
binary X509 encoding for RSA key send during transport handshake (#105)
This commit is contained in:
committed by
GitHub
parent
3187bc8140
commit
cddff78719
@@ -71,4 +71,4 @@ readCreateKey = do
|
||||
errorExit e = putStrLn (e <> ": " <> path) >> exitFailure
|
||||
|
||||
publicKeyHash :: C.PublicKey -> B.ByteString
|
||||
publicKeyHash = C.serializeKeyHash . C.getKeyHash . C.serializePubKey
|
||||
publicKeyHash = C.serializeKeyHash . C.getKeyHash . C.binaryEncodePubKey
|
||||
|
||||
@@ -22,7 +22,7 @@ To establish the session keys and base IVs, the server should have an asymmetric
|
||||
|
||||
The handshake sequence is the following:
|
||||
|
||||
1. Once the connection is established, the server sends its public 2048 bit key to the client. TODO currently the key will be sent as a line terminated with CRLF, using ad-hoc key serialization we use.
|
||||
1. Once the connection is established, the server sends transport_header and its public RSA key encoded in X509 binary format to the client.
|
||||
2. The client compares the hash of the received key with the hash it already has (e.g. received as part of connection invitation or server in NEW command). If the hash does not match, the client must terminate the connection. TODO as the hash is optional in server syntax at the moment, hash comparison will be optional as well. Probably it should become required.
|
||||
3. If the hash is the same, the client should generate random symmetric AES keys and base IVs that will be used as session keys/IVs by the client and the server.
|
||||
4. The client then should encrypt these symmetric keys and base IVs with the public key that the server sent, and send to the server the result of the encryption: `rsa-encrypt(snd-aes-key, snd-base-iv, rcv-aes-key, rcv-base-iv)`. `snd-aes-key` and `snd-base-iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv-aes-key` and `rcv-base-iv` will be used by the client to decrypt **received** messages and by the server to encrypt them.
|
||||
@@ -34,6 +34,11 @@ All the subsequent data both from the client and from the server should be sent
|
||||
Each transport block sent by the client and the server has this syntax:
|
||||
|
||||
```abnf
|
||||
transport_header = block_size protocol key_size
|
||||
block_size = 4*4(OCTET) ; 4-byte block size sent by the server, currently the client rejects if > 65536 bytes
|
||||
protocol = 2*2(OCTET) ; currently it is 0, that means binary RSA key
|
||||
key_size = 2*2(OCTET) ; the encoded key size in bytes (binary encoded in X509 standard)
|
||||
|
||||
transport_block = aes_body_auth_tag aes_encrypted_body ; fixed at 8192 bits
|
||||
aes_encrypted_body = 1*OCTET
|
||||
aes_body_auth_tag = 16*16(OCTET)
|
||||
|
||||
@@ -33,10 +33,12 @@ module Simplex.Messaging.Crypto
|
||||
decryptAES,
|
||||
serializePrivKey,
|
||||
serializePubKey,
|
||||
binaryEncodePubKey,
|
||||
serializeKeyHash,
|
||||
getKeyHash,
|
||||
privKeyP,
|
||||
pubKeyP,
|
||||
binaryPubKeyP,
|
||||
keyHashP,
|
||||
authTagSize,
|
||||
authTagToBS,
|
||||
@@ -179,8 +181,8 @@ generateKeyPair size = loop
|
||||
then loop
|
||||
else pure (PublicKey k, mkPrivateKey pk)
|
||||
|
||||
rsaPrivateSize :: PrivateKey k => k -> Int
|
||||
rsaPrivateSize = R.public_size . R.private_pub . rsaPrivateKey
|
||||
privateKeySize :: PrivateKey k => k -> Int
|
||||
privateKeySize = R.public_size . R.private_pub . rsaPrivateKey
|
||||
|
||||
publicKey :: FullPrivateKey -> PublicKey
|
||||
publicKey = PublicKey . R.private_pub . rsaPrivateKey
|
||||
@@ -258,7 +260,7 @@ encrypt k paddedSize msg = do
|
||||
|
||||
decrypt :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
decrypt pk msg'' = do
|
||||
let (encHeader, msg') = B.splitAt (rsaPrivateSize pk) msg''
|
||||
let (encHeader, msg') = B.splitAt (privateKeySize pk) msg''
|
||||
header <- decryptOAEP pk encHeader
|
||||
Header {aesKey, ivBytes, authTag, msgSize} <- except $ parseHeader header
|
||||
msg <- decryptAES aesKey ivBytes msg' authTag
|
||||
@@ -342,6 +344,9 @@ serializePrivKey pk = "rsa:" <> encodePrivKey pk
|
||||
pubKeyP :: Parser PublicKey
|
||||
pubKeyP = keyP decodePubKey <|> legacyPubKeyP
|
||||
|
||||
binaryPubKeyP :: Parser PublicKey
|
||||
binaryPubKeyP = either fail pure . binaryDecodePubKey =<< A.takeByteString
|
||||
|
||||
privKeyP :: PrivateKey k => Parser k
|
||||
privKeyP = keyP decodePrivKey <|> legacyPrivKeyP
|
||||
|
||||
@@ -382,28 +387,34 @@ safeRsaPrivateKey (size, n, d) =
|
||||
}
|
||||
|
||||
encodePubKey :: PublicKey -> ByteString
|
||||
encodePubKey = encodeKey . PubKeyRSA . rsaPublicKey
|
||||
encodePubKey = encode . binaryEncodePubKey
|
||||
|
||||
binaryEncodePubKey :: PublicKey -> ByteString
|
||||
binaryEncodePubKey = binaryEncodeKey . PubKeyRSA . rsaPublicKey
|
||||
|
||||
encodePrivKey :: PrivateKey k => k -> ByteString
|
||||
encodePrivKey = encodeKey . PrivKeyRSA . rsaPrivateKey
|
||||
encodePrivKey = encode . binaryEncodeKey . PrivKeyRSA . rsaPrivateKey
|
||||
|
||||
encodeKey :: ASN1Object a => a -> ByteString
|
||||
encodeKey k = encode . toStrict . encodeASN1 DER $ toASN1 k []
|
||||
binaryEncodeKey :: ASN1Object a => a -> ByteString
|
||||
binaryEncodeKey k = toStrict . encodeASN1 DER $ toASN1 k []
|
||||
|
||||
decodePubKey :: ByteString -> Either String PublicKey
|
||||
decodePubKey s =
|
||||
decodeKey s >>= \case
|
||||
decodePubKey = binaryDecodePubKey <=< decode
|
||||
|
||||
binaryDecodePubKey :: ByteString -> Either String PublicKey
|
||||
binaryDecodePubKey =
|
||||
binaryDecodeKey >=> \case
|
||||
(PubKeyRSA k, []) -> Right $ PublicKey k
|
||||
r -> keyError r
|
||||
|
||||
decodePrivKey :: PrivateKey k => ByteString -> Either String k
|
||||
decodePrivKey s =
|
||||
decodeKey s >>= \case
|
||||
decodePrivKey =
|
||||
decode >=> binaryDecodeKey >=> \case
|
||||
(PrivKeyRSA pk, []) -> Right $ mkPrivateKey pk
|
||||
r -> keyError r
|
||||
|
||||
decodeKey :: ASN1Object a => ByteString -> Either String (a, [ASN1])
|
||||
decodeKey s = fromASN1 =<< first show . decodeASN1 DER . fromStrict =<< decode s
|
||||
binaryDecodeKey :: ASN1Object a => ByteString -> Either String (a, [ASN1])
|
||||
binaryDecodeKey = fromASN1 <=< first show . decodeASN1 DER . fromStrict
|
||||
|
||||
keyError :: (a, [ASN1]) -> Either String b
|
||||
keyError = \case
|
||||
|
||||
@@ -32,7 +32,7 @@ import GHC.IO.Exception (IOErrorType (..))
|
||||
import GHC.IO.Handle.Internals (ioe_EOF)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Network.Socket
|
||||
import Network.Transport.Internal (encodeWord32)
|
||||
import Network.Transport.Internal (decodeNum16, decodeNum32, encodeEnum16, encodeEnum32, encodeWord32)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers (parse, parseAll, parseRead1)
|
||||
import Simplex.Messaging.Util (bshow, liftError)
|
||||
@@ -172,6 +172,7 @@ data HandshakeError
|
||||
| DECRYPT
|
||||
| VERSION
|
||||
| RSA_KEY
|
||||
| HEADER
|
||||
| AES_KEYS
|
||||
| BAD_HASH
|
||||
| MAJOR_VERSION
|
||||
@@ -233,15 +234,18 @@ makeNextIV SessionKey {baseIV, counter} = atomically $ do
|
||||
-- The numbers in function names refer to the steps in the document
|
||||
serverHandshake :: Handle -> C.FullKeyPair -> ExceptT TransportError IO THandle
|
||||
serverHandshake h (k, pk) = do
|
||||
liftIO sendPublicKey_1
|
||||
liftIO sendHeaderAndPublicKey_1
|
||||
encryptedKeys <- receiveEncryptedKeys_4
|
||||
HandshakeKeys {sndKey, rcvKey} <- decryptParseKeys_5 encryptedKeys
|
||||
th <- liftIO $ transportHandle h rcvKey sndKey -- keys are swapped here
|
||||
th <- liftIO $ transportHandle h rcvKey sndKey transportBlockSize -- keys are swapped here
|
||||
sendWelcome_6 th
|
||||
pure th
|
||||
where
|
||||
sendPublicKey_1 :: IO ()
|
||||
sendPublicKey_1 = putLn h $ C.serializePubKey k
|
||||
sendHeaderAndPublicKey_1 :: IO ()
|
||||
sendHeaderAndPublicKey_1 = do
|
||||
let sKey = C.binaryEncodePubKey k
|
||||
header = TransportHeader {blockSize = transportBlockSize, keySize = B.length sKey}
|
||||
B.hPut h $ binaryTransportHeader header <> sKey
|
||||
receiveEncryptedKeys_4 :: ExceptT TransportError IO ByteString
|
||||
receiveEncryptedKeys_4 =
|
||||
liftIO (B.hGet h $ C.publicKeySize k) >>= \case
|
||||
@@ -258,20 +262,25 @@ serverHandshake h (k, pk) = do
|
||||
-- The numbers in function names refer to the steps in the document
|
||||
clientHandshake :: Handle -> Maybe C.KeyHash -> ExceptT TransportError IO THandle
|
||||
clientHandshake h keyHash = do
|
||||
k <- getPublicKey_1_2
|
||||
(k, blkSize) <- getHeaderAndPublicKey_1_2
|
||||
keys@HandshakeKeys {sndKey, rcvKey} <- liftIO generateKeys_3
|
||||
sendEncryptedKeys_4 k keys
|
||||
th <- liftIO $ transportHandle h sndKey rcvKey
|
||||
th <- liftIO $ transportHandle h sndKey rcvKey blkSize
|
||||
getWelcome_6 th >>= checkVersion
|
||||
pure th
|
||||
where
|
||||
getPublicKey_1_2 :: ExceptT TransportError IO C.PublicKey
|
||||
getPublicKey_1_2 = do
|
||||
s <- liftIO $ getLn h
|
||||
getHeaderAndPublicKey_1_2 :: ExceptT TransportError IO (C.PublicKey, Int)
|
||||
getHeaderAndPublicKey_1_2 = do
|
||||
header <- liftIO (B.hGet h transportHeaderSize)
|
||||
TransportHeader {blockSize, keySize} <- liftEither $ parse transportHeaderP (TEHandshake HEADER) header
|
||||
when (blockSize < transportBlockSize || blockSize > maxTransportBlockSize) $
|
||||
throwError $ TEHandshake HEADER
|
||||
s <- liftIO $ B.hGet h keySize
|
||||
maybe (pure ()) (validateKeyHash_2 s) keyHash
|
||||
liftEither $ parseKey s
|
||||
key <- liftEither $ parseKey s
|
||||
pure (key, blockSize)
|
||||
parseKey :: ByteString -> Either TransportError C.PublicKey
|
||||
parseKey = first (const $ TEHandshake RSA_KEY) . parseAll C.pubKeyP
|
||||
parseKey = first (const $ TEHandshake RSA_KEY) . parseAll C.binaryPubKeyP
|
||||
validateKeyHash_2 :: ByteString -> C.KeyHash -> ExceptT TransportError IO ()
|
||||
validateKeyHash_2 k kHash
|
||||
| C.getKeyHash k == kHash = pure ()
|
||||
@@ -296,6 +305,35 @@ clientHandshake h keyHash = do
|
||||
when (major smpVersion > major currentSMPVersion) . throwE $
|
||||
TEHandshake MAJOR_VERSION
|
||||
|
||||
data TransportHeader = TransportHeader {blockSize :: Int, keySize :: Int}
|
||||
deriving (Eq, Show)
|
||||
|
||||
binaryRsaTransport :: Int
|
||||
binaryRsaTransport = 0
|
||||
|
||||
transportBlockSize :: Int
|
||||
transportBlockSize = 8192
|
||||
|
||||
maxTransportBlockSize :: Int
|
||||
maxTransportBlockSize = 65536
|
||||
|
||||
transportHeaderSize :: Int
|
||||
transportHeaderSize = 8
|
||||
|
||||
binaryTransportHeader :: TransportHeader -> ByteString
|
||||
binaryTransportHeader TransportHeader {blockSize, keySize} =
|
||||
encodeEnum32 blockSize <> encodeEnum16 binaryRsaTransport <> encodeEnum16 keySize
|
||||
|
||||
transportHeaderP :: Parser TransportHeader
|
||||
transportHeaderP = TransportHeader <$> int32 <* binaryRsaTransportP <*> int16
|
||||
where
|
||||
int32 = decodeNum32 <$> A.take 4
|
||||
int16 = decodeNum16 <$> A.take 2
|
||||
binaryRsaTransportP = binaryRsa <$> int16
|
||||
binaryRsa :: Int -> Parser Int
|
||||
binaryRsa 0 = pure 0
|
||||
binaryRsa _ = fail "unknown transport mode"
|
||||
|
||||
serializeHandshakeKeys :: HandshakeKeys -> ByteString
|
||||
serializeHandshakeKeys HandshakeKeys {sndKey, rcvKey} =
|
||||
serializeKey sndKey <> serializeKey rcvKey
|
||||
@@ -315,8 +353,8 @@ handshakeKeysP = HandshakeKeys <$> keyP <*> keyP
|
||||
parseHandshakeKeys :: ByteString -> Either TransportError HandshakeKeys
|
||||
parseHandshakeKeys = parse handshakeKeysP $ TEHandshake AES_KEYS
|
||||
|
||||
transportHandle :: Handle -> SessionKey -> SessionKey -> IO THandle
|
||||
transportHandle h sk rk = do
|
||||
transportHandle :: Handle -> SessionKey -> SessionKey -> Int -> IO THandle
|
||||
transportHandle h sk rk blockSize = do
|
||||
sndCounter <- newTVarIO 0
|
||||
rcvCounter <- newTVarIO 0
|
||||
pure
|
||||
@@ -324,5 +362,5 @@ transportHandle h sk rk = do
|
||||
{ handle = h,
|
||||
sndKey = sk {counter = sndCounter},
|
||||
rcvKey = rk {counter = rcvCounter},
|
||||
blockSize = 8192
|
||||
blockSize
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ infixl 4 <$$>
|
||||
bshow :: Show a => a -> ByteString
|
||||
bshow = B.pack . show
|
||||
|
||||
liftIOEither :: (MonadUnliftIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither a = liftIO a >>= liftEither
|
||||
|
||||
liftError :: (MonadUnliftIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
|
||||
liftError :: (MonadIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
|
||||
liftError f = liftEitherError f . runExceptT
|
||||
|
||||
liftEitherError :: (MonadUnliftIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
|
||||
liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
|
||||
liftEitherError f a = liftIOEither (first f <$> a)
|
||||
|
||||
@@ -31,10 +31,10 @@ testPort :: ServiceName
|
||||
testPort = "5000"
|
||||
|
||||
teshKeyHashStr :: B.ByteString
|
||||
teshKeyHashStr = "p1xa/XuzchgqomEL6RX+Me+fX096w50V7nJPAA0wpDE="
|
||||
teshKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
|
||||
|
||||
teshKeyHash :: Maybe C.KeyHash
|
||||
teshKeyHash = Just "p1xa/XuzchgqomEL6RX+Me+fX096w50V7nJPAA0wpDE="
|
||||
teshKeyHash = Just "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
|
||||
|
||||
testSMPClient :: MonadUnliftIO m => (THandle -> m a) -> m a
|
||||
testSMPClient client =
|
||||
|
||||
Reference in New Issue
Block a user