Skip to content

Commit bbcde51

Browse files
authored
WPB-18695 Return a specific error for leaf signature verification failure (#4665)
1 parent 160d47c commit bbcde51

File tree

14 files changed

+159
-50
lines changed

14 files changed

+159
-50
lines changed

changelog.d/5-internal/WPB-18695

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Dedicated error label for MLS leaf node signature validation failure

integration/test/Test/MLS.hs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module Test.MLS where
44

55
import API.Brig (claimKeyPackages, deleteClient)
66
import API.Galley
7+
import Data.Bits
8+
import qualified Data.ByteString as B
79
import qualified Data.ByteString.Base64 as Base64
810
import qualified Data.ByteString.Char8 as B8
911
import qualified Data.Map as Map
@@ -941,3 +943,30 @@ testRemoteAddLegacy domain = do
941943
void $ uploadNewKeyPackage suite bob1
942944
convId <- createNewGroup suite alice1
943945
void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle
946+
947+
testInvalidLeafNodeSignature :: (HasCallStack) => App ()
948+
testInvalidLeafNodeSignature = do
949+
alice <- randomUser OwnDomain def
950+
[creator, other] <- traverse (createMLSClient def) (replicate 2 alice)
951+
(_, conv) <- createSelfGroup def creator
952+
convId <- objConvId conv
953+
void $ createAddCommit creator convId [alice] >>= sendAndConsumeCommitBundle
954+
955+
void $ uploadNewKeyPackage def other
956+
957+
mp <- createExternalCommit convId other Nothing
958+
bindResponse (postMLSCommitBundle other (mkBundle mp {message = makeSignatureCorrupt mp.message})) $ \resp -> do
959+
resp.status `shouldMatchInt` 400
960+
resp.json %. "label" `shouldMatch` "mls-invalid-leaf-node-signature"
961+
where
962+
-- This is a hack to make the signature invalid.
963+
-- It works as long as the format of the MLS message does not change
964+
-- in any way that changes the offset of the signature.
965+
-- If this test ever starts flaking, we should consider
966+
-- factoring the MLS code out of wire-api into a separate shared package
967+
-- and use it in this test to invalidate the signature.
968+
makeSignatureCorrupt :: ByteString -> ByteString
969+
makeSignatureCorrupt bs = case B.splitAt 0xb0 bs of
970+
(left, right) -> case B.uncons right of
971+
Just (h, t) -> left <> B.singleton (h `xor` 0x01) <> t
972+
Nothing -> bs

libs/wire-api/src/Wire/API/Error/Brig.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ data BrigError
119119
| UserGroupNotATeamAdmin
120120
| UserGroupMemberIsNotInTheSameTeam
121121
| DuplicateEntry
122+
| MLSInvalidLeafNodeSignature
122123

123124
instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where
124125
addToOpenApi = addStaticErrorToSwagger @(MapError e)
@@ -355,3 +356,5 @@ type instance MapError 'UserGroupNotATeamAdmin = 'StaticError 403 "user-group-wr
355356
type instance MapError 'UserGroupMemberIsNotInTheSameTeam = 'StaticError 400 "user-group-invalid" "Only team members of the same team can be added to a user group."
356357

357358
type instance MapError 'DuplicateEntry = 'StaticError 409 "duplicate-entry" "Entry already exists"
359+
360+
type instance MapError 'MLSInvalidLeafNodeSignature = 'StaticError 400 "mls-invalid-leaf-node-signature" "Invalid leaf node signature in key package"

libs/wire-api/src/Wire/API/Error/Galley.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ data GalleyError
151151
| ChannelsNotEnabled
152152
| NotAnMlsConversation
153153
| MLSReadReceiptsNotAllowed
154+
| MLSInvalidLeafNodeSignature
154155
deriving (Show, Eq, Generic)
155156
deriving (FromJSON, ToJSON) via (CustomEncoded GalleyError)
156157

@@ -347,6 +348,8 @@ type instance MapError 'NotAnMlsConversation = 'StaticError 403 "not-mls-convers
347348

348349
type instance MapError 'MLSReadReceiptsNotAllowed = 'StaticError 403 "mls-receipts-not-allowed" "Read receipts on MLS conversations are not allowed"
349350

351+
type instance MapError 'MLSInvalidLeafNodeSignature = 'StaticError 400 "mls-invalid-leaf-node-signature" "Invalid leaf node signature"
352+
350353
--------------------------------------------------------------------------------
351354
-- Team Member errors
352355

libs/wire-api/src/Wire/API/MLS/Validation.hs

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ module Wire.API.MLS.Validation
1919
( -- * Main key package validation function
2020
validateKeyPackage,
2121
validateLeafNode,
22+
ValidationError (..),
2223
)
2324
where
2425

2526
import Control.Applicative
2627
import Control.Error.Util
2728
import Data.ByteArray qualified as BA
28-
import Data.Text qualified as T
29-
import Data.Text.Lazy qualified as LT
30-
import Data.Text.Lazy.Builder qualified as LT
31-
import Data.Text.Lazy.Builder.Int qualified as LT
3229
import Data.X509 qualified as X509
3330
import Imports
3431
import Wire.API.MLS.Capabilities
@@ -39,20 +36,17 @@ import Wire.API.MLS.LeafNode
3936
import Wire.API.MLS.Lifetime
4037
import Wire.API.MLS.ProtocolVersion
4138
import Wire.API.MLS.Serialisation
39+
import Wire.API.MLS.Validation.Error
4240

4341
validateKeyPackage ::
4442
Maybe ClientIdentity ->
4543
KeyPackage ->
46-
Either Text (CipherSuiteTag, Lifetime)
44+
Either ValidationError (CipherSuiteTag, Lifetime)
4745
validateKeyPackage mIdentity kp = do
4846
-- get ciphersuite
4947
cs <-
5048
maybe
51-
( Left
52-
( "Unsupported ciphersuite 0x"
53-
<> LT.toStrict (LT.toLazyText (LT.hexadecimal kp.cipherSuite.cipherSuiteNumber))
54-
)
55-
)
49+
(Left (UnsupportedCipherSuite kp.cipherSuite.cipherSuiteNumber))
5650
pure
5751
$ cipherSuiteTag kp.cipherSuite
5852

@@ -65,11 +59,11 @@ validateKeyPackage mIdentity kp = do
6559
kp.tbs
6660
kp.signature_
6761
)
68-
$ Left "Invalid KeyPackage signature"
62+
$ Left InvalidKeyPackageSignature
6963

7064
-- validate protocol version
7165
maybe
72-
(Left "Unsupported protocol version")
66+
(Left UnsupportedProtocolVersion)
7367
pure
7468
(pvTag (kp.protocolVersion) >>= guard . (== ProtocolMLS10))
7569

@@ -79,7 +73,7 @@ validateKeyPackage mIdentity kp = do
7973
lt <- case kp.leafNode.source of
8074
LeafNodeSourceKeyPackage lt -> pure lt
8175
-- unreachable
82-
_ -> Left "Unexpected leaf node source"
76+
_ -> Left UnexpectedLeafNodeSource
8377

8478
pure (cs, lt)
8579

@@ -88,7 +82,7 @@ validateLeafNode ::
8882
Maybe ClientIdentity ->
8983
LeafNodeTBSExtra ->
9084
LeafNode ->
91-
Either Text ()
85+
Either ValidationError ()
9286
validateLeafNode cs mIdentity extra leafNode = do
9387
let tbs = LeafNodeTBS leafNode.core extra
9488
unless
@@ -99,27 +93,25 @@ validateLeafNode cs mIdentity extra leafNode = do
9993
(mkRawMLS tbs)
10094
leafNode.signature_
10195
)
102-
$ Left "Invalid LeafNode signature"
96+
$ Left InvalidLeafNodeSignature
10397

10498
validateCredential cs leafNode.signatureKey mIdentity leafNode.credential
10599
validateSource extra.tag leafNode.source
106100
validateCapabilities (credentialTag leafNode.credential) leafNode.capabilities
107101

108-
validateCredential :: CipherSuiteTag -> ByteString -> Maybe ClientIdentity -> Credential -> Either Text ()
102+
validateCredential :: CipherSuiteTag -> ByteString -> Maybe ClientIdentity -> Credential -> Either ValidationError ()
109103
validateCredential cs pkey mIdentity cred = do
110104
-- FUTUREWORK: check signature in the case of an x509 credential
111105
(identity, mkey) <-
112106
either credentialError pure $
113107
credentialIdentityAndKey cred
114108
traverse_ (validateCredentialKey (csSignatureScheme cs) pkey) mkey
115109
unless (maybe True (identity ==) mIdentity) $
116-
Left "client identity does not match credential identity"
110+
Left IdentityMismatch
117111
where
118-
credentialError e =
119-
Left $
120-
"Failed to parse identity: " <> e
112+
credentialError e = Left $ FailedToParseIdentity e
121113

122-
validateCredentialKey :: SignatureSchemeTag -> ByteString -> X509.PubKey -> Either Text ()
114+
validateCredentialKey :: SignatureSchemeTag -> ByteString -> X509.PubKey -> Either ValidationError ()
123115
validateCredentialKey Ed25519 pk1 (X509.PubKeyEd25519 pk2) = validateCredentialKeyBS pk1 (BA.convert pk2)
124116
validateCredentialKey Ecdsa_secp256r1_sha256 pk1 (X509.PubKeyEC pk2) =
125117
case pk2.pubkeyEC_pub of
@@ -131,28 +123,28 @@ validateCredentialKey Ecdsa_secp521r1_sha512 pk1 (X509.PubKeyEC pk2) =
131123
case pk2.pubkeyEC_pub of
132124
X509.SerializedPoint bs -> validateCredentialKeyBS pk1 bs
133125
validateCredentialKey ss _ _ =
134-
Left $
135-
"Certificate signature scheme " <> T.pack (show ss) <> " does not match client's public key"
126+
Left $ SchemeMismatch ss
136127

137-
validateCredentialKeyBS :: ByteString -> ByteString -> Either Text ()
128+
validateCredentialKeyBS :: ByteString -> ByteString -> Either ValidationError ()
138129
validateCredentialKeyBS pk1 pk2 =
139-
note "Certificate public key does not match client's" $
130+
note PublicKeyMismatch $
140131
guard (pk1 == pk2)
141132

142-
validateSource :: LeafNodeSourceTag -> LeafNodeSource -> Either Text ()
133+
validateSource :: LeafNodeSourceTag -> LeafNodeSource -> Either ValidationError ()
143134
validateSource t s = do
144135
let t' = leafNodeSourceTag s
145136
if t == t'
146137
then pure ()
147138
else
148139
Left $
149-
"Expected '"
150-
<> t.name
151-
<> "' source, got '"
152-
<> t'.name
153-
<> "'"
140+
LeafNodeSourceTagMisMatch $
141+
"Expected '"
142+
<> t.name
143+
<> "' source, got '"
144+
<> t'.name
145+
<> "'"
154146

155-
validateCapabilities :: CredentialTag -> Capabilities -> Either Text ()
147+
validateCapabilities :: CredentialTag -> Capabilities -> Either ValidationError ()
156148
validateCapabilities ctag caps =
157149
unless (fromMLSEnum ctag `elem` caps.credentials) $
158-
Left "missing BasicCredential capability"
150+
Left BasicCredentialCapabilityMissing
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-- This file is part of the Wire Server implementation.
2+
--
3+
-- Copyright (C) 2025 Wire Swiss GmbH <[email protected]>
4+
--
5+
-- This program is free software: you can redistribute it and/or modify it under
6+
-- the terms of the GNU Affero General Public License as published by the Free
7+
-- Software Foundation, either version 3 of the License, or (at your option) any
8+
-- later version.
9+
--
10+
-- This program is distributed in the hope that it will be useful, but WITHOUT
11+
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
-- details.
14+
--
15+
-- You should have received a copy of the GNU Affero General Public License along
16+
-- with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
module Wire.API.MLS.Validation.Error where
19+
20+
import Data.Text qualified as T
21+
import Data.Text.Lazy qualified as LT
22+
import Data.Text.Lazy.Builder qualified as LT
23+
import Data.Text.Lazy.Builder.Int qualified as LT
24+
import Imports
25+
import Wire.API.MLS.CipherSuite (SignatureSchemeTag)
26+
27+
data ValidationError
28+
= InvalidKeyPackageSignature
29+
| UnsupportedCipherSuite Word16
30+
| InvalidLeafNodeSignature
31+
| FailedToParseIdentity Text
32+
| SchemeMismatch SignatureSchemeTag
33+
| LeafNodeSourceTagMisMatch Text
34+
| UnsupportedProtocolVersion
35+
| UnexpectedLeafNodeSource
36+
| IdentityMismatch
37+
| PublicKeyMismatch
38+
| BasicCredentialCapabilityMissing
39+
deriving (Show, Eq, Generic)
40+
41+
toText :: ValidationError -> Text
42+
toText InvalidKeyPackageSignature = "Invalid KeyPackage signature"
43+
toText (UnsupportedCipherSuite cs) = "Unsupported ciphersuite 0x" <> LT.toStrict (LT.toLazyText (LT.hexadecimal cs))
44+
toText InvalidLeafNodeSignature = "Invalid LeafNode signature"
45+
toText (FailedToParseIdentity err) = "Failed to parse identity: " <> err
46+
toText (SchemeMismatch ss) = "Certificate signature scheme " <> T.pack (show ss) <> " does not match client's public key"
47+
toText (LeafNodeSourceTagMisMatch err) = err
48+
toText UnsupportedProtocolVersion = "Unsupported protocol version"
49+
toText UnexpectedLeafNodeSource = "Unexpected leaf node source"
50+
toText IdentityMismatch = "Client identity does not match credential identity"
51+
toText PublicKeyMismatch = "Certificate public key does not match client's"
52+
toText BasicCredentialCapabilityMissing = "Missing BasicCredential capability"

libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type MLSMessagingAPI =
8080
:> CanThrow MLSProposalFailure
8181
:> CanThrow NonFederatingBackends
8282
:> CanThrow UnreachableBackends
83+
:> CanThrow 'MLSInvalidLeafNodeSignature
8384
:> "messages"
8485
:> ZLocalUser
8586
:> ZClient
@@ -116,6 +117,7 @@ type MLSMessagingAPI =
116117
:> CanThrow NonFederatingBackends
117118
:> CanThrow UnreachableBackends
118119
:> CanThrow GroupIdVersionNotSupported
120+
:> CanThrow MLSInvalidLeafNodeSignature
119121
:> "commit-bundles"
120122
:> ZLocalUser
121123
:> ZClient

libs/wire-api/wire-api.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ library
137137
Wire.API.MLS.Servant
138138
Wire.API.MLS.SubConversation
139139
Wire.API.MLS.Validation
140+
Wire.API.MLS.Validation.Error
140141
Wire.API.MLS.Welcome
141142
Wire.API.Notification
142143
Wire.API.OAuth

services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,21 @@ import Data.Time.Clock.POSIX
3636
import Imports
3737
import Wire.API.Error
3838
import Wire.API.Error.Brig
39+
import Wire.API.Error.Brig qualified as E
3940
import Wire.API.MLS.CipherSuite
4041
import Wire.API.MLS.Credential
4142
import Wire.API.MLS.KeyPackage
4243
import Wire.API.MLS.Lifetime
4344
import Wire.API.MLS.Serialisation
4445
import Wire.API.MLS.Validation
46+
import Wire.API.MLS.Validation.Error (toText)
4547

4648
validateUploadedKeyPackage ::
4749
ClientIdentity ->
4850
RawMLS KeyPackage ->
4951
Handler r (KeyPackageRef, CipherSuiteTag, KeyPackageData)
5052
validateUploadedKeyPackage identity kp = do
51-
(cs, lt) <- either mlsProtocolError pure $ validateKeyPackage (Just identity) kp.value
53+
(cs, lt) <- either mlsProtocolErrorFromValidationError pure $ validateKeyPackage (Just identity) kp.value
5254

5355
validateLifetime lt
5456

@@ -95,6 +97,10 @@ validateLifetime' now mMaxLifetime lt = do
9597
when (tsPOSIX (ltNotAfter lt) > now + maxLifetime) $
9698
Left "Key package expiration time is too far in the future"
9799

100+
mlsProtocolErrorFromValidationError :: ValidationError -> Handler r a
101+
mlsProtocolErrorFromValidationError InvalidLeafNodeSignature = throwStd (errorToWai @E.MLSInvalidLeafNodeSignature)
102+
mlsProtocolErrorFromValidationError err = mlsProtocolError (toText err)
103+
98104
mlsProtocolError :: Text -> Handler r a
99105
mlsProtocolError msg =
100106
throwStd . dynErrorToWai $

services/galley/src/Galley/API/MLS/Commit/Core.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import Wire.API.MLS.LeafNode
6767
import Wire.API.MLS.Serialisation
6868
import Wire.API.MLS.SubConversation
6969
import Wire.API.MLS.Validation
70+
import Wire.API.MLS.Validation.Error (toText)
7071
import Wire.API.User.Client
7172
import Wire.NotificationSubsystem
7273

@@ -102,7 +103,8 @@ type HasProposalActionEffects r =
102103

103104
getCommitData ::
104105
( HasProposalEffects r,
105-
Member (ErrorS 'MLSProposalNotFound) r
106+
Member (ErrorS 'MLSProposalNotFound) r,
107+
Member (ErrorS MLSInvalidLeafNodeSignature) r
106108
) =>
107109
SenderIdentity ->
108110
Local ConvOrSubConv ->
@@ -246,7 +248,8 @@ checkUpdatePath ::
246248
Member (Error MLSProtocolError) r,
247249
Member (Error FederationError) r,
248250
Member BrigAccess r,
249-
Member FederatorAccess r
251+
Member FederatorAccess r,
252+
Member (ErrorS MLSInvalidLeafNodeSignature) r
250253
) =>
251254
Local ConvOrSubConv ->
252255
SenderIdentity ->
@@ -257,9 +260,10 @@ checkUpdatePath lConvOrSub senderIdentity ciphersuite path = for_ senderIdentity
257260
let groupId = cnvmlsGroupId (tUnqualified lConvOrSub).mlsMeta
258261
let extra = LeafNodeTBSExtraCommit groupId index
259262
case validateLeafNode ciphersuite (Just senderIdentity.client) extra path.leaf.value of
263+
Left InvalidLeafNodeSignature -> throwS @'MLSInvalidLeafNodeSignature
260264
Left errMsg ->
261265
throw $
262-
mlsProtocolError ("Tried to add invalid LeafNode: " <> errMsg)
266+
mlsProtocolError ("Tried to add invalid LeafNode: " <> toText errMsg)
263267
Right _ -> pure ()
264268
clientInfo <-
265269
getSingleClientInfo

0 commit comments

Comments
 (0)