diff --git a/changelog.d/0-release-notes/new-ciphersuites b/changelog.d/0-release-notes/new-ciphersuites new file mode 100644 index 00000000000..e972b33c4a1 --- /dev/null +++ b/changelog.d/0-release-notes/new-ciphersuites @@ -0,0 +1 @@ +Added support for 3 more MLS ciphersuites. To enable MLS, all supported signature schemes (ed25519 and the three ecdsa variants) now need to have private keys specified in galley's configuration file. diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 1ff99379292..1043cc17416 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -75,10 +75,11 @@ data: federationDomain: {{ .settings.federationDomain }} {{- if $.Values.secrets.mlsPrivateKeys }} mlsPrivateKeyPaths: - {{- if $.Values.secrets.mlsPrivateKeys.removal.ed25519 }} removal: ed25519: "/etc/wire/galley/secrets/removal_ed25519.pem" - {{- end }} + ecdsa_secp256r1_sha256: "/etc/wire/galley/secrets/removal_ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "/etc/wire/galley/secrets/removal_ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "/etc/wire/galley/secrets/removal_ecdsa_secp521r1_sha512.pem" {{- end }} disabledAPIVersions: {{ toJson .settings.disabledAPIVersions }} {{- if .settings.featureFlags }} diff --git a/charts/galley/templates/secret.yaml b/charts/galley/templates/secret.yaml index 9cc45c39d1e..84995f51bc5 100644 --- a/charts/galley/templates/secret.yaml +++ b/charts/galley/templates/secret.yaml @@ -13,6 +13,15 @@ data: {{- if .Values.secrets.mlsPrivateKeys.removal.ed25519 }} removal_ed25519.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ed25519 | b64enc | quote }} {{- end -}} + {{- if .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp256r1_sha256 }} + removal_ecdsa_secp256r1_sha256.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp256r1_sha256 | b64enc | quote }} + {{- end -}} + {{- if .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp384r1_sha384 }} + removal_ecdsa_secp384r1_sha384.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp384r1_sha384 | b64enc | quote }} + {{- end -}} + {{- if .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp521r1_sha512 }} + removal_ecdsa_secp521r1_sha512.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp521r1_sha512 | b64enc | quote }} + {{- end -}} {{- end -}} {{- if $.Values.config.enableFederation }} diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 595d683b68f..81253bfc61f 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -24,15 +24,26 @@ For example: mlsPrivateKeyPaths: removal: ed25519: /etc/secrets/ed25519.pem + ecdsa_secp256r1_sha256: /etc/secrets/ecdsa_secp256r1_sha256 + ecdsa_secp384r1_sha384: /etc/secrets/ecdsa_secp384r1_sha384 + ecdsa_secp521r1_sha512: /etc/secrets/ecdsa_secp521r1_sha512 ``` A simple way to generate an ed25519 private key, discarding the corresponding certificate, is to run the following command: ``` -openssl req -nodes -newkey ed25519 -keyout ed25519.pem -out /dev/null -subj / +openssl genpkey -algorithm ed25519 ``` +ECDSA private keys can be generated with: + +``` +openssl genpkey -algorithm ec -genparam dsa -pkeyopt ec_paramgen_curve:P-256 +``` + +and similar (replace `P-256` with `P-384` or `P-521`). + ## Feature flags > Also see [Wire docs](https://docs.wire.com/how-to/install/team-feature-settings.html) where some of the feature flags are documented from an operations point of view. diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 4a7ff60a809..6db0fa026cc 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -252,6 +252,28 @@ galley: -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3qjgQ9U+/rTBObn9 + tXSVi2UtHksRDXmQ1VOszFZfjryhRANCAATNkLmZZLyORf5D3PUOxt+rkJTE5vuD + aCqZ7sE5NSN8InRRwuQ1kv0oblDVeQA89ZlHqyxx75JPK+/air7Z1n5I + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBLwv3i5LDz9b++O0iw + QAit/Uq7L5PWPgKN99wCm8xkZnuyqWujXW4wvlVUVlZWgh2hZANiAAT0+RXKE31c + VxdYazaVopY50/nV9c18uRdqoENBvtxuD6oDtJtU6oCS/Htkd8JEArTQ9ZHqq144 + yRjuc3d2CqvJmEA/lzIBk9wnz+lghFhvB4TkSHvvLyEBc9DZvhb4EEQ= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiaEARm5BMaRct1xj + MlemUHijWGAoHtNMhSttSr4jo0WxMwfMnvnDQJSlO2Zs4Tzum2j5eO34EHu6MUrv + qquZYwyhgYkDgYYABAHuvCV/+gJitvAbDwgrBHZJ41oy8Lc+wPIM7Yp6s/vTzTsG + Klo7aMdkx6DUjv/56tVD9bZNulFAjwS8xoIyWg8NSAE1ofo8CBvN1XGZOWuMYjEh + zLrZADduEnOvayw5sEvm135WC0vWjPJaYwKZPdDIXUz9ILJPgNe3gEUvHsDEXvdX + lw== + -----END PRIVATE KEY----- rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index d64db7af1cd..811a4c7a706 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -223,6 +223,11 @@ listConversations user cnvs = do req & addJSONObject ["qualified_ids" .= cnvs] +getMLSPublicKeys :: (HasCallStack, MakesValue user) => user -> App Response +getMLSPublicKeys user = do + req <- baseRequest user Galley Versioned "/mls/public-keys" + submit "GET" req + postMLSMessage :: HasCallStack => ClientIdentity -> ByteString -> App Response postMLSMessage cid msg = do req <- baseRequest cid Galley Versioned "/mls/messages" diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 23686c5f18b..f89a4434fff 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -80,6 +80,8 @@ mlscli :: (HasCallStack) => ClientIdentity -> [String] -> Maybe ByteString -> Ap mlscli cid args mbstdin = do groupOut <- randomFileName let substOut = argSubst "" groupOut + cs <- (.ciphersuite) <$> getMLSState + let scheme = csSignatureScheme cs gs <- getClientGroupState cid @@ -88,23 +90,24 @@ mlscli cid args mbstdin = do Just groupData -> do fn <- toRandomFile groupData pure (argSubst "" fn) - store <- maybe randomFileName toRandomFile gs.keystore + store <- case Map.lookup scheme gs.keystore of + Nothing -> do + bd <- getBaseDir + liftIO $ createDirectory (bd cid2Str cid) + + -- initialise new keystore + path <- randomFileName + ctype <- make gs.credType & asString + void $ runCli path ["init", "--ciphersuite", cs.code, "-t", ctype, cid2Str cid] Nothing + pure path + Just s -> toRandomFile s let args' = map (substIn . substOut) args for_ args' $ \arg -> when (arg `elem` ["", ""]) $ assertFailure ("Unbound arg: " <> arg) - out <- - spawn - ( proc - "mls-test-cli" - ( ["--store", store] - <> args' - ) - ) - mbstdin - + out <- runCli store args' mbstdin setGroup <- do groupOutWritten <- liftIO $ doesFileExist groupOut if groupOutWritten @@ -114,12 +117,23 @@ mlscli cid args mbstdin = do else pure id setStore <- do storeData <- liftIO (BS.readFile store) - pure $ \x -> x {keystore = Just storeData} + pure $ \x -> x {keystore = Map.insert scheme storeData x.keystore} - setClientGroupState cid ((setGroup . setStore) gs) + setClientGroupState cid (setGroup (setStore gs)) pure out +runCli :: HasCallStack => FilePath -> [String] -> Maybe ByteString -> App ByteString +runCli store args mStdin = + spawn + ( proc + "mls-test-cli" + ( ["--store", store] + <> args + ) + ) + mStdin + argSubst :: String -> String -> String -> String argSubst from to_ s = if s == from then to_ else s @@ -130,16 +144,11 @@ createWireClient u = do c <- addClient u def {lastPrekey = Just lpk} >>= getJSON 201 mkClientIdentity u c -data CredentialType = BasicCredentialType | X509CredentialType - -instance MakesValue CredentialType where - make BasicCredentialType = make "basic" - make X509CredentialType = make "x509" - -instance (HasTests x) => HasTests (CredentialType -> x) where - mkTests m n s f x = - mkTests m (n <> "[ctype=basic]") s f (x BasicCredentialType) - <> mkTests m (n <> "[ctype=x509]") s f (x X509CredentialType) +-- data CredentialType = BasicCredentialType | X509CredentialType +-- +-- instance MakesValue CredentialType where +-- make BasicCredentialType = make "basic" +-- make X509CredentialType = make "x509" data InitMLSClient = InitMLSClient {credType :: CredentialType} @@ -147,28 +156,21 @@ data InitMLSClient = InitMLSClient instance Default InitMLSClient where def = InitMLSClient {credType = BasicCredentialType} -initMLSClient :: (HasCallStack) => InitMLSClient -> ClientIdentity -> App () -initMLSClient opts cid = do - bd <- getBaseDir - mls <- getMLSState - liftIO $ createDirectory (bd cid2Str cid) - ctype <- make opts.credType & asString - void $ mlscli cid ["init", "--ciphersuite", mls.ciphersuite.code, "-t", ctype, cid2Str cid] Nothing - -- | Create new mls client and register with backend. createMLSClient :: (MakesValue u, HasCallStack) => InitMLSClient -> u -> App ClientIdentity createMLSClient opts u = do cid <- createWireClient u - initMLSClient opts cid + setClientGroupState cid def {credType = opts.credType} -- set public key pkey <- mlscli cid ["public-key"] Nothing + ciphersuite <- (.ciphersuite) <$> getMLSState bindResponse ( updateClient cid def { mlsPublicKeys = - Just (object ["ed25519" .= T.decodeUtf8 (Base64.encode pkey)]) + Just (object [csSignatureScheme ciphersuite .= T.decodeUtf8 (Base64.encode pkey)]) } ) $ \resp -> resp.status `shouldMatchInt` 200 @@ -177,8 +179,7 @@ createMLSClient opts u = do -- | create and upload to backend uploadNewKeyPackage :: (HasCallStack) => ClientIdentity -> App String uploadNewKeyPackage cid = do - mls <- getMLSState - (kp, ref) <- generateKeyPackage cid mls.ciphersuite + (kp, ref) <- generateKeyPackage cid -- upload key package bindResponse (uploadKeyPackages cid [kp]) $ \resp -> @@ -186,8 +187,9 @@ uploadNewKeyPackage cid = do pure ref -generateKeyPackage :: (HasCallStack) => ClientIdentity -> Ciphersuite -> App (ByteString, String) -generateKeyPackage cid suite = do +generateKeyPackage :: HasCallStack => ClientIdentity -> App (ByteString, String) +generateKeyPackage cid = do + suite <- (.ciphersuite) <$> getMLSState kp <- mlscli cid ["key-package", "create", "--ciphersuite", suite.code] Nothing ref <- B8.unpack . Base64.encode <$> mlscli cid ["key-package", "ref", "-"] (Just kp) fp <- keyPackageFile cid ref @@ -244,8 +246,11 @@ resetGroup cid conv = do resetClientGroup :: ClientIdentity -> String -> App () resetClientGroup cid gid = do - removalKeyPath <- asks (.removalKeyPath) mls <- getMLSState + removalKeyPaths <- asks (.removalKeyPaths) + removalKeyPath <- + assertOne $ + Map.lookup (csSignatureScheme mls.ciphersuite) removalKeyPaths void $ mlscli cid @@ -706,7 +711,7 @@ spawn cp minput = do getClientGroupState :: (HasCallStack) => ClientIdentity -> App ClientGroupState getClientGroupState cid = do mls <- getMLSState - pure $ Map.findWithDefault emptyClientGroupState cid mls.clientGroupState + pure $ Map.findWithDefault def cid mls.clientGroupState setClientGroupState :: (HasCallStack) => ClientIdentity -> ClientGroupState -> App () setClientGroupState cid g = @@ -761,6 +766,17 @@ createApplicationMessage cid messageContent = do setMLSCiphersuite :: Ciphersuite -> App () setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} +withCiphersuite :: HasCallStack => Ciphersuite -> App a -> App a +withCiphersuite suite action = do + suite0 <- (.ciphersuite) <$> getMLSState + setMLSCiphersuiteIO <- appToIOKleisli setMLSCiphersuite + actionIO <- appToIO action + liftIO $ + bracket + (setMLSCiphersuiteIO suite) + (const (setMLSCiphersuiteIO suite0)) + (const actionIO) + leaveCurrentConv :: (HasCallStack) => ClientIdentity -> @@ -778,7 +794,7 @@ leaveCurrentConv cid = do { members = Set.difference mls.members (Set.singleton cid) } -getCurrentConv :: (HasCallStack) => ClientIdentity -> App Value +getCurrentConv :: HasCallStack => ClientIdentity -> App Value getCurrentConv cid = do mls <- getMLSState (conv, mSubId) <- objSubConv mls.convId diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 049d3d8d4a7..d01607eea2c 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -4,8 +4,10 @@ module Test.MLS where import API.Brig (claimKeyPackages, deleteClient) import API.Galley +import qualified Data.Aeson.KeyMap as KM import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Set as Set import qualified Data.Text.Encoding as T import MLS.Util import Notifications @@ -323,9 +325,12 @@ testAddUserSimple :: HasCallStack => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def {credType = ctype}) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] + bob1 <- createMLSClient def {credType = ctype} bob + void $ uploadNewKeyPackage bob1 + [alice1, bob2] <- traverse (createMLSClient def {credType = ctype}) [alice, bob] + + traverse_ uploadNewKeyPackage [bob2] (_, qcnv) <- createNewGroup alice1 resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -365,8 +370,9 @@ testRemoteAddUser = do resp.status `shouldMatchInt` 500 resp.json %. "label" `shouldMatch` "federation-not-implemented" -testRemoteRemoveClient :: HasCallStack => App () -testRemoteRemoveClient = do +testRemoteRemoveClient :: HasCallStack => Ciphersuite -> App () +testRemoteRemoveClient suite = do + setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] void $ uploadNewKeyPackage bob1 @@ -478,8 +484,9 @@ testRemoveClientsIncomplete = do err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" -testAdminRemovesUserFromConv :: HasCallStack => App () -testAdminRemovesUserFromConv = do +testAdminRemovesUserFromConv :: HasCallStack => Ciphersuite -> App () +testAdminRemovesUserFromConv suite = do + setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] @@ -674,7 +681,7 @@ testCommitNotReferencingAllProposals = do testUnsupportedCiphersuite :: HasCallStack => App () testUnsupportedCiphersuite = do - setMLSCiphersuite (Ciphersuite "0x0002") + setMLSCiphersuite (Ciphersuite "0x0003") alice <- randomUser OwnDomain def alice1 <- createMLSClient def alice void $ createNewGroup alice1 @@ -684,3 +691,59 @@ testUnsupportedCiphersuite = do bindResponse (postMLSCommitBundle alice1 (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-protocol-error" + +testBackendRemoveProposal :: HasCallStack => Ciphersuite -> Domain -> App () +testBackendRemoveProposal suite domain = do + setMLSCiphersuite suite + [alice, bob] <- createAndConnectUsers [OwnDomain, domain] + (alice1 : bobClients) <- traverse (createMLSClient def) [alice, bob, bob] + traverse_ uploadNewKeyPackage bobClients + void $ createNewGroup alice1 + + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + let isRemoveProposalFor :: Int -> Value -> App Bool + isRemoveProposalFor index e = + isNewMLSMessageNotif e &&~ do + msgData <- e %. "payload.0.data" & asByteString + msg <- showMessage alice1 msgData + fieldEquals msg "message.content.body.Proposal.Remove.removed" index + + withWebSocket alice1 \ws -> do + deleteUser bob + for_ (zip [1 ..] bobClients) \(index, _) -> do + void $ consumeMessageWithPredicate (isRemoveProposalFor index) alice1 Nothing ws + + bobUser <- asString $ bob %. "id" + modifyMLSState $ \mls -> + mls + { members = Set.filter (\m -> m.user /= bobUser) mls.members + } + + -- alice commits the external proposals + r <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + shouldBeEmpty $ r %. "events" + +testPublicKeys :: HasCallStack => App () +testPublicKeys = do + alice <- randomUserId OwnDomain + let expectedKeys = + [ "ed25519", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512" + ] + bindResponse (getMLSPublicKeys alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + (KM.keys <$> asObject (resp.json %. "removal")) `shouldMatchSet` expectedKeys + +testPublicKeysMLSNotEnabled :: HasCallStack => App () +testPublicKeysMLSNotEnabled = withModifiedBackend + def + { galleyCfg = removeField "settings.mlsPrivateKeyPaths" + } + $ \domain -> do + alice <- randomUserId domain + bindResponse (getMLSPublicKeys alice) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-not-enabled" diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 69e8c84fbdf..cf6b721db88 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -54,12 +54,9 @@ testKeyPackageMultipleCiphersuites = do testKeyPackageUploadNoKey :: App () testKeyPackageUploadNoKey = do alice <- randomUser OwnDomain def - alice1 <- do - cid <- createWireClient alice - initMLSClient def cid - pure cid + alice1 <- createWireClient alice - (kp, _) <- generateKeyPackage alice1 def + (kp, _) <- generateKeyPackage alice1 -- if we upload a keypackage without a key, -- we get a bad request @@ -184,6 +181,7 @@ testKeyPackageRemoteClaim = do testKeyPackageCount :: HasCallStack => Ciphersuite -> App () testKeyPackageCount cs = do + setMLSCiphersuite cs alice <- randomUser OwnDomain def alice1 <- createMLSClient def alice @@ -192,7 +190,7 @@ testKeyPackageCount cs = do resp.json %. "count" `shouldMatchInt` 0 let count = 10 - kps <- map fst <$> replicateM count (generateKeyPackage alice1 cs) + kps <- map fst <$> replicateM count (generateKeyPackage alice1) void $ uploadKeyPackages alice1 kps >>= getBody 201 bindResponse (countKeyPackages cs alice1) $ \resp -> do @@ -201,11 +199,11 @@ testKeyPackageCount cs = do testUnsupportedCiphersuite :: HasCallStack => App () testUnsupportedCiphersuite = do - let suite = Ciphersuite "0x0002" + let suite = Ciphersuite "0x0003" setMLSCiphersuite suite bob <- randomUser OwnDomain def bob1 <- createMLSClient def bob - (kp, _) <- generateKeyPackage bob1 suite + (kp, _) <- generateKeyPackage bob1 bindResponse (uploadKeyPackages bob1 [kp]) $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-protocol-error" @@ -223,12 +221,12 @@ testReplaceKeyPackages = do -- setup: upload a batch of key packages for each ciphersuite void $ - replicateM 4 (fmap fst (generateKeyPackage alice1 def)) + replicateM 4 (fmap fst (generateKeyPackage alice1)) >>= uploadKeyPackages alice1 >>= getBody 201 setMLSCiphersuite suite void $ - replicateM 5 (fmap fst (generateKeyPackage alice1 suite)) + replicateM 5 (fmap fst (generateKeyPackage alice1)) >>= uploadKeyPackages alice1 >>= getBody 201 @@ -237,7 +235,7 @@ testReplaceKeyPackages = do do -- generate a new batch of key packages - (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1 suite) + (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1) -- replace old key packages with new void $ replaceKeyPackages alice1 [suite] kps >>= getBody 201 @@ -262,7 +260,7 @@ testReplaceKeyPackages = do do -- replenish key packages for the second ciphersuite void $ - replicateM 5 (fmap fst (generateKeyPackage alice1 suite)) + replicateM 5 (fmap fst (generateKeyPackage alice1)) >>= uploadKeyPackages alice1 >>= getBody 201 @@ -270,8 +268,10 @@ testReplaceKeyPackages = do checkCount suite 5 -- replace all key packages with fresh ones - kps1 <- replicateM 2 (fmap fst (generateKeyPackage alice1 def)) - kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1 suite)) + setMLSCiphersuite def + kps1 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) + setMLSCiphersuite suite + kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) void $ replaceKeyPackages alice1 [def, suite] (kps1 <> kps2) >>= getBody 201 diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 0e85badb2f7..18c8f6633ff 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -1,9 +1,12 @@ module Testlib.App where import Control.Monad.Reader +import Control.Monad.Trans.Maybe import qualified Control.Retry as Retry import Data.Aeson hiding ((.=)) +import Data.Bool (bool) import Data.IORef +import Data.Maybe import qualified Data.Text as T import qualified Data.Yaml as Yaml import GHC.Exception @@ -70,3 +73,17 @@ instance MakesValue FedDomain where -- backwards-compatible way so everybody can benefit. retryT :: App a -> App a retryT action = Retry.recoverAll (Retry.exponentialBackoff 8000 <> Retry.limitRetries 10) (const action) + +-- | make Bool lazy +liftBool :: Functor f => f Bool -> BoolT f +liftBool = MaybeT . fmap (bool Nothing (Just ())) + +-- | make Bool strict +unliftBool :: Functor f => BoolT f -> f Bool +unliftBool = fmap isJust . runMaybeT + +-- | lazy (&&) +(&&~) :: App Bool -> App Bool -> App Bool +b1 &&~ b2 = unliftBool $ liftBool b1 *> liftBool b2 + +type BoolT f = MaybeT f () diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index a6aa6a5d45a..6d67fc9c3b8 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -105,7 +105,7 @@ mkGlobalEnv cfgFile = do gDefaultAPIVersion = 6, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), - gRemovalKeyPath = error "Uninitialised removal key path", + gRemovalKeyPaths = mempty, gBackendResourcePool = resourcePool, gRabbitMQConfig = intConfig.rabbitmq, gTempDir = tempDir, @@ -143,7 +143,7 @@ mkEnv ge = do defaultAPIVersion = gDefaultAPIVersion ge, manager = gManager ge, servicesCwdBase = gServicesCwdBase ge, - removalKeyPath = gRemovalKeyPath ge, + removalKeyPaths = gRemovalKeyPaths ge, prekeys = pks, lastPrekeys = lpks, mls = mls, @@ -164,11 +164,8 @@ create ioRef = Nothing -> error "No resources available" Just (r, s') -> (s', r) -emptyClientGroupState :: ClientGroupState -emptyClientGroupState = ClientGroupState Nothing Nothing - allCiphersuites :: [Ciphersuite] -allCiphersuites = [Ciphersuite "0x0001"] -- TODO fix testsMLS.testAddUserSimple for "0xf031" +allCiphersuites = map Ciphersuite ["0x0001", "0xf031", "0x0002", "0x0007"] mkMLSState :: Codensity IO MLSState mkMLSState = Codensity $ \k -> diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 59aa2400ff4..2d56c97dbcf 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -412,3 +412,7 @@ instance MakesValue ClientIdentity where "id" .= cid.user, "client_id" .= cid.client ] + +instance MakesValue CredentialType where + make BasicCredentialType = make "basic" + make X509CredentialType = make "x509" diff --git a/integration/test/Testlib/PTest.hs b/integration/test/Testlib/PTest.hs index 1aa478b720f..d8e36fe4db3 100644 --- a/integration/test/Testlib/PTest.hs +++ b/integration/test/Testlib/PTest.hs @@ -24,3 +24,8 @@ instance HasTests x => HasTests (Ciphersuite -> x) where [ mkTests m (n <> "[suite=" <> suite.code <> "]") s f (x suite) | suite <- allCiphersuites ] + +instance (HasTests x) => HasTests (CredentialType -> x) where + mkTests m n s f x = + mkTests m (n <> "[ctype=basic]") s f (x BasicCredentialType) + <> mkTests m (n <> "[ctype=x509]") s f (x X509CredentialType) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index de574293eec..2a5a4520133 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -8,12 +8,15 @@ import Control.Monad.IO.Class import Control.Monad.Reader import Crypto.Error import qualified Crypto.PubKey.Ed25519 as Ed25519 +import Data.Aeson (Value) import Data.ByteArray (convert) +import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.Foldable import Data.Function import Data.Functor import Data.List +import qualified Data.Map as Map import Data.PEM import Data.Time.Clock import Data.Traversable (for) @@ -106,30 +109,63 @@ main = do createGlobalEnv :: FilePath -> Codensity IO GlobalEnv createGlobalEnv cfg = do genv0 <- mkGlobalEnv cfg + -- Run codensity locally here, because we only need the environment to get at + -- Galley's configuration. Accessing the environment has the side effect of + -- creating a temporary mls directory, which we don't need here. - pubkey <- liftIO . lowerCodensity $ do + let removalKeysDir = gTempDir genv0 "removal-keys" + keys <- liftIO . lowerCodensity $ do env <- mkEnv genv0 + liftIO $ createDirectoryIfMissing True removalKeysDir liftIO . runAppWithEnv env $ do config <- readServiceConfig Galley - relPath <- config %. "settings.mlsPrivateKeyPaths.removal.ed25519" & asString - path <- asks \env' -> case env'.servicesCwdBase of - Nothing -> relPath - Just dir -> dir "galley" relPath - bs <- liftIO $ B.readFile path - pems <- case pemParseBS bs of - Left err -> assertFailure $ "Could not parse removal key PEM: " <> err - Right x -> pure x - asn1 <- pemContent <$> assertOne pems - -- quick and dirty ASN.1 decoding: assume the key is of the correct - -- format, and simply skip the 16 byte header - let bytes = B.drop 16 asn1 - priv <- liftIO . throwCryptoErrorIO $ Ed25519.secretKey bytes - pure (convert (Ed25519.toPublic priv)) + for + [ ("ed25519", loadEd25519Key), + ("ecdsa_secp256r1_sha256", loadEcKey "ecdsa_secp256r1_sha256" 73), + ("ecdsa_secp384r1_sha384", loadEcKey "ecdsa_secp384r1_sha384" 88), + ("ecdsa_secp521r1_sha512", loadEcKey "ecdsa_secp521r1_sha512" 108) + ] + $ \(sigScheme, load) -> do + key <- load config + let path = removalKeysDir (sigScheme <> ".key") + liftIO $ B.writeFile path key + pure (sigScheme, path) -- save removal key to a temporary file - let removalPath = gTempDir genv0 "removal.key" - liftIO $ B.writeFile removalPath pubkey - pure genv0 {gRemovalKeyPath = removalPath} + pure genv0 {gRemovalKeyPaths = Map.fromList keys} + +getPrivateKeyPath :: Value -> String -> App FilePath +getPrivateKeyPath config signatureScheme = do + relPath <- config %. "settings.mlsPrivateKeyPaths.removal" %. signatureScheme & asString + asks \env' -> case env'.servicesCwdBase of + Nothing -> relPath + Just dir -> dir "galley" relPath + +loadEcKey :: String -> Int -> Value -> App ByteString +loadEcKey sigScheme offset config = do + path <- getPrivateKeyPath config sigScheme + bs <- liftIO $ B.readFile path + pems <- case pemParseBS bs of + Left err -> assertFailure $ "Could not parse removal key PEM: " <> err + Right x -> pure x + asn1 <- pemContent <$> assertOne pems + -- quick and dirty ASN.1 decoding: assume the key is of the correct + -- format, and simply skip the header + pure $ B.drop offset asn1 + +loadEd25519Key :: Value -> App ByteString +loadEd25519Key config = do + path <- getPrivateKeyPath config "ed25519" + bs <- liftIO $ B.readFile path + pems <- case pemParseBS bs of + Left err -> assertFailure $ "Could not parse removal key PEM: " <> err + Right x -> pure x + asn1 <- pemContent <$> assertOne pems + -- quick and dirty ASN.1 decoding: assume the key is of the correct + -- format, and simply skip the 16 byte header + let bytes = B.drop 16 asn1 + priv <- liftIO . throwCryptoErrorIO $ Ed25519.secretKey bytes + pure (convert (Ed25519.toPublic priv)) runTests :: [(String, x, y, App ())] -> Maybe FilePath -> FilePath -> IO () runTests tests mXMLOutput cfg = do diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index ed18a345dd3..61cdbd8dfc7 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -107,7 +107,7 @@ data GlobalEnv = GlobalEnv gDefaultAPIVersion :: Int, gManager :: HTTP.Manager, gServicesCwdBase :: Maybe FilePath, - gRemovalKeyPath :: FilePath, + gRemovalKeyPaths :: Map String FilePath, gBackendResourcePool :: ResourcePool BackendResource, gRabbitMQConfig :: RabbitMQConfig, gTempDir :: FilePath, @@ -200,7 +200,8 @@ data Env = Env defaultAPIVersion :: Int, manager :: HTTP.Manager, servicesCwdBase :: Maybe FilePath, - removalKeyPath :: FilePath, + -- | paths to removal keys by signature scheme + removalKeyPaths :: Map String FilePath, prekeys :: IORef [(Int, String)], lastPrekeys :: IORef [String], mls :: IORef MLSState, @@ -221,6 +222,9 @@ data Response = Response instance HasField "json" Response (App Aeson.Value) where getField response = maybe (assertFailure "Response has no json body") pure response.jsonBody +data CredentialType = BasicCredentialType | X509CredentialType + deriving (Eq, Show) + data ClientIdentity = ClientIdentity { domain :: String, user :: String, @@ -236,10 +240,27 @@ instance Default Ciphersuite where data ClientGroupState = ClientGroupState { group :: Maybe ByteString, - keystore :: Maybe ByteString + -- | mls-test-cli stores by signature scheme + keystore :: Map String ByteString, + credType :: CredentialType } deriving (Show) +instance Default ClientGroupState where + def = + ClientGroupState + { group = Nothing, + keystore = mempty, + credType = BasicCredentialType + } + +csSignatureScheme :: Ciphersuite -> String +csSignatureScheme (Ciphersuite code) = case code of + "0x0002" -> "ecdsa_secp256r1_sha256" + "0x0005" -> "ecdsa_secp521r1_sha512" + "0x0007" -> "ecdsa_secp384r1_sha384" + _ -> "ed25519" + data MLSProtocol = MLSProtocolMLS | MLSProtocolMixed deriving (Eq, Show) diff --git a/libs/polysemy-wire-zoo/default.nix b/libs/polysemy-wire-zoo/default.nix index e5a88e3be19..ebf7e8de8c5 100644 --- a/libs/polysemy-wire-zoo/default.nix +++ b/libs/polysemy-wire-zoo/default.nix @@ -8,6 +8,7 @@ , bytestring , cassandra-util , containers +, crypton , gitignoreSource , HsOpenSSL , hspec @@ -36,6 +37,7 @@ mkDerivation { base bytestring cassandra-util + crypton HsOpenSSL hspec imports diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 5e346eb0ea2..981e6d8b552 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -85,6 +85,7 @@ library , base >=4.6 && <5.0 , bytestring , cassandra-util + , crypton , HsOpenSSL , hspec , imports diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs index aa58643eea1..da054b545ed 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs @@ -22,9 +22,11 @@ module Wire.Sem.Random bytes, uuid, scimTokenId, + liftRandom, ) where +import Crypto.Random.Types import Data.Id (ScimTokenId) import Data.UUID (UUID) import Imports @@ -34,5 +36,6 @@ data Random m a where Bytes :: Int -> Random m ByteString Uuid :: Random m UUID ScimTokenId :: Random m ScimTokenId + LiftRandom :: (forall mr. MonadRandom mr => mr a) -> Random m a makeSem ''Random diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs index 5fcf31709d8..e64815799f4 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs @@ -35,3 +35,4 @@ randomToIO = interpret $ \case Bytes i -> embed $ randBytes i Uuid -> embed $ UUID.nextRandom ScimTokenId -> embed $ randomId @IO + LiftRandom m -> embed @IO $ m diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 87dc1bf9b84..94c6d14f725 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -7,6 +7,7 @@ , aeson-diff , aeson-pretty , aeson-qq +, asn1-encoding , async , attoparsec , base @@ -115,6 +116,7 @@ mkDerivation { src = gitignoreSource ./.; libraryHaskellDepends = [ aeson + asn1-encoding async attoparsec base diff --git a/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs b/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs index 521217f7c53..20f346fb054 100644 --- a/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs +++ b/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs @@ -24,7 +24,7 @@ module Wire.API.MLS.AuthenticatedContent ) where -import Crypto.PubKey.Ed25519 +import Crypto.Random.Types import Imports hiding (cs) import Wire.API.MLS.CipherSuite import Wire.API.MLS.Context @@ -85,8 +85,15 @@ taggedSenderMembershipTag _ = Nothing -- | Craft a message with the backend itself as a sender. Return the message and its ref. mkSignedPublicMessage :: - SecretKey -> PublicKey -> GroupId -> Epoch -> TaggedSender -> FramedContentData -> PublicMessage -mkSignedPublicMessage priv pub gid epoch sender payload = + forall ss m. + (IsSignatureScheme ss, MonadRandom m) => + KeyPair ss -> + GroupId -> + Epoch -> + TaggedSender -> + FramedContentData -> + m PublicMessage +mkSignedPublicMessage kp gid epoch sender payload = do let framedContent = mkRawMLS FramedContent @@ -103,9 +110,10 @@ mkSignedPublicMessage priv pub gid epoch sender payload = content = framedContent, groupContext = Nothing } - sig = signWithLabel "FramedContentTBS" priv pub (mkRawMLS tbs) - in PublicMessage - { content = framedContent, - authData = mkRawMLS (FramedContentAuthData sig Nothing), - membershipTag = taggedSenderMembershipTag sender - } + sig <- signWithLabel @ss "FramedContentTBS" kp (mkRawMLS tbs) + pure + PublicMessage + { content = framedContent, + authData = mkRawMLS (FramedContentAuthData sig Nothing), + membershipTag = taggedSenderMembershipTag sender + } diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index fc06a3d708f..c67ee1672dc 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -27,12 +27,17 @@ module Wire.API.MLS.CipherSuite -- * MLS signature schemes SignatureScheme (..), + IsSignatureScheme, SignatureSchemeTag (..), + SignatureSchemeCurve, signatureScheme, signatureSchemeName, signatureSchemeTag, csSignatureScheme, + -- * Key pairs + KeyPair, + -- * Utilities csHash, csVerifySignatureWithLabel, @@ -45,10 +50,13 @@ import Cassandra.CQL import Control.Applicative import Control.Error (note) import Control.Lens ((?~)) +import Crypto.ECC hiding (KeyPair) import Crypto.Error import Crypto.Hash (hashWith) import Crypto.Hash.Algorithms +import Crypto.PubKey.ECDSA qualified as ECDSA import Crypto.PubKey.Ed25519 qualified as Ed25519 +import Crypto.Random.Types import Data.Aeson qualified as Aeson import Data.Aeson.Types (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) import Data.Aeson.Types qualified as Aeson @@ -69,6 +77,7 @@ import Data.Text.Lazy.Builder.Int qualified as LT import Data.Word import Imports hiding (cs) import Web.HttpApiData +import Wire.API.MLS.ECDSA qualified as ECDSA import Wire.API.MLS.Serialisation import Wire.Arbitrary @@ -106,6 +115,9 @@ instance FromByteString CipherSuite where data CipherSuiteTag = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + | MLS_128_DHKEMP256_AES128GCM_SHA256_P256 + | MLS_256_DHKEMP384_AES256GCM_SHA384_P384 + | MLS_256_DHKEMP521_AES256GCM_SHA512_P521 | MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 deriving stock (Bounded, Enum, Eq, Show, Generic, Ord) deriving (Arbitrary) via (GenericUniform CipherSuiteTag) @@ -143,18 +155,34 @@ cipherSuiteTag cs = listToMaybe $ do -- | Inverse of 'cipherSuiteTag' tagCipherSuite :: CipherSuiteTag -> CipherSuite -tagCipherSuite MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = CipherSuite 1 +tagCipherSuite MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = CipherSuite 0x1 +tagCipherSuite MLS_128_DHKEMP256_AES128GCM_SHA256_P256 = CipherSuite 0x2 +tagCipherSuite MLS_256_DHKEMP384_AES256GCM_SHA384_P384 = CipherSuite 0x7 +tagCipherSuite MLS_256_DHKEMP521_AES256GCM_SHA512_P521 = CipherSuite 0x5 tagCipherSuite MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = CipherSuite 0xf031 -csHash :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString -csHash MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = sha256Hash -csHash MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = sha256Hash +data SomeHashAlgorithm where + SomeHashAlgorithm :: HashAlgorithm a => a -> SomeHashAlgorithm -sha256Hash :: ByteString -> RawMLS a -> ByteString -sha256Hash ctx value = convert . hashWith SHA256 . encodeMLS' $ RefHashInput ctx value +csHashAlgorithm :: CipherSuiteTag -> SomeHashAlgorithm +csHashAlgorithm MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = SomeHashAlgorithm SHA256 +csHashAlgorithm MLS_128_DHKEMP256_AES128GCM_SHA256_P256 = SomeHashAlgorithm SHA256 +csHashAlgorithm MLS_256_DHKEMP384_AES256GCM_SHA384_P384 = SomeHashAlgorithm SHA384 +csHashAlgorithm MLS_256_DHKEMP521_AES256GCM_SHA512_P521 = SomeHashAlgorithm SHA512 +csHashAlgorithm MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = SomeHashAlgorithm SHA256 + +csHash :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString +csHash cs ctx value = case csHashAlgorithm cs of + SomeHashAlgorithm a -> convert . hashWith a . encodeMLS' $ RefHashInput ctx value csVerifySignature :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString -> Bool csVerifySignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = ed25519VerifySignature +csVerifySignature MLS_128_DHKEMP256_AES128GCM_SHA256_P256 = + ECDSA.verifySignature (Proxy @Curve_P256R1) SHA256 +csVerifySignature MLS_256_DHKEMP384_AES256GCM_SHA384_P384 = + ECDSA.verifySignature (Proxy @Curve_P384R1) SHA384 +csVerifySignature MLS_256_DHKEMP521_AES256GCM_SHA512_P521 = + ECDSA.verifySignature (Proxy @Curve_P521R1) SHA512 csVerifySignature MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = ed25519VerifySignature ed25519VerifySignature :: ByteString -> RawMLS a -> ByteString -> Bool @@ -198,14 +226,44 @@ csVerifySignatureWithLabel :: csVerifySignatureWithLabel cs pub label x sig = csVerifySignature cs pub (mkRawMLS (mkSignContent label x)) sig --- FUTUREWORK: generalise to arbitrary ciphersuites -signWithLabel :: ByteString -> Ed25519.SecretKey -> Ed25519.PublicKey -> RawMLS a -> ByteString -signWithLabel sigLabel priv pub x = BA.convert $ Ed25519.sign priv pub (encodeMLS' (mkSignContent sigLabel x)) +signWithLabel :: + forall ss a m. + (IsSignatureScheme ss, MonadRandom m) => + ByteString -> + KeyPair ss -> + RawMLS a -> + m ByteString +signWithLabel sigLabel kp x = sign @ss kp (encodeMLS' (mkSignContent sigLabel x)) csSignatureScheme :: CipherSuiteTag -> SignatureSchemeTag csSignatureScheme MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = Ed25519 +csSignatureScheme MLS_128_DHKEMP256_AES128GCM_SHA256_P256 = Ecdsa_secp256r1_sha256 +csSignatureScheme MLS_256_DHKEMP384_AES256GCM_SHA384_P384 = Ecdsa_secp384r1_sha384 +csSignatureScheme MLS_256_DHKEMP521_AES256GCM_SHA512_P521 = Ecdsa_secp521r1_sha512 csSignatureScheme MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = Ed25519 +type family PrivateKey (ss :: SignatureSchemeTag) + +type instance PrivateKey Ed25519 = Ed25519.SecretKey + +type instance PrivateKey Ecdsa_secp256r1_sha256 = ECDSA.PrivateKey Curve_P256R1 + +type instance PrivateKey Ecdsa_secp384r1_sha384 = ECDSA.PrivateKey Curve_P384R1 + +type instance PrivateKey Ecdsa_secp521r1_sha512 = ECDSA.PrivateKey Curve_P521R1 + +type family PublicKey (ss :: SignatureSchemeTag) + +type instance PublicKey Ed25519 = Ed25519.PublicKey + +type instance PublicKey Ecdsa_secp256r1_sha256 = ECDSA.PublicKey Curve_P256R1 + +type instance PublicKey Ecdsa_secp384r1_sha384 = ECDSA.PublicKey Curve_P384R1 + +type instance PublicKey Ecdsa_secp521r1_sha512 = ECDSA.PublicKey Curve_P521R1 + +type KeyPair (ss :: SignatureSchemeTag) = (PrivateKey ss, PublicKey ss) + -- | A TLS signature scheme. -- -- See . @@ -216,10 +274,43 @@ newtype SignatureScheme = SignatureScheme {unSignatureScheme :: Word16} signatureScheme :: SignatureSchemeTag -> SignatureScheme signatureScheme = SignatureScheme . signatureSchemeNumber -data SignatureSchemeTag = Ed25519 +data SignatureSchemeTag + = Ed25519 + | Ecdsa_secp256r1_sha256 + | Ecdsa_secp384r1_sha384 + | Ecdsa_secp521r1_sha512 deriving stock (Bounded, Enum, Eq, Ord, Show, Generic) deriving (Arbitrary) via GenericUniform SignatureSchemeTag +class IsSignatureScheme (ss :: SignatureSchemeTag) where + sign :: MonadRandom m => KeyPair ss -> ByteString -> m ByteString + +instance IsSignatureScheme 'Ed25519 where + sign (priv, pub) = pure . BA.convert . Ed25519.sign priv pub + +instance IsSignatureScheme 'Ecdsa_secp256r1_sha256 where + sign (priv, _) = + fmap (ECDSA.encodeSignature (Proxy @Curve_P256R1)) + . ECDSA.sign (Proxy @Curve_P256R1) priv SHA256 + +instance IsSignatureScheme 'Ecdsa_secp384r1_sha384 where + sign (priv, _) = + fmap (ECDSA.encodeSignature (Proxy @Curve_P384R1)) + . ECDSA.sign (Proxy @Curve_P384R1) priv SHA384 + +instance IsSignatureScheme 'Ecdsa_secp521r1_sha512 where + sign (priv, _) = + fmap (ECDSA.encodeSignature (Proxy @Curve_P521R1)) + . ECDSA.sign (Proxy @Curve_P521R1) priv SHA512 + +type family SignatureSchemeCurve (ss :: SignatureSchemeTag) + +type instance SignatureSchemeCurve 'Ecdsa_secp256r1_sha256 = Curve_P256R1 + +type instance SignatureSchemeCurve 'Ecdsa_secp384r1_sha384 = Curve_P384R1 + +type instance SignatureSchemeCurve 'Ecdsa_secp521r1_sha512 = Curve_P521R1 + instance Cql SignatureSchemeTag where ctype = Tagged TextColumn toCql = CqlText . signatureSchemeName @@ -230,9 +321,15 @@ instance Cql SignatureSchemeTag where signatureSchemeNumber :: SignatureSchemeTag -> Word16 signatureSchemeNumber Ed25519 = 0x807 +signatureSchemeNumber Ecdsa_secp256r1_sha256 = 0x403 +signatureSchemeNumber Ecdsa_secp384r1_sha384 = 0x503 +signatureSchemeNumber Ecdsa_secp521r1_sha512 = 0x603 signatureSchemeName :: SignatureSchemeTag -> Text signatureSchemeName Ed25519 = "ed25519" +signatureSchemeName Ecdsa_secp256r1_sha256 = "ecdsa_secp256r1_sha256" +signatureSchemeName Ecdsa_secp384r1_sha384 = "ecdsa_secp384r1_sha384" +signatureSchemeName Ecdsa_secp521r1_sha512 = "ecdsa_secp521r1_sha512" signatureSchemeTag :: SignatureScheme -> Maybe SignatureSchemeTag signatureSchemeTag (SignatureScheme n) = getAlt $ diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index f4b74c6f1d3..d369727f3e1 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -17,12 +17,8 @@ module Wire.API.MLS.Credential where -import Control.Error.Util import Control.Lens ((?~)) -import Data.Aeson (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) -import Data.Aeson qualified as Aeson -import Data.Aeson.Types qualified as Aeson -import Data.Bifunctor +import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Binary import Data.Binary.Get import Data.Binary.Parser @@ -158,42 +154,3 @@ instance SerialiseMLS ClientIdentity where mkClientIdentity :: Qualified UserId -> ClientId -> ClientIdentity mkClientIdentity (Qualified uid domain) = ClientIdentity domain uid - --- | Possible uses of a private key in the context of MLS. -data SignaturePurpose - = -- | Creating external remove proposals. - RemovalPurpose - deriving (Eq, Ord, Show, Bounded, Enum) - -signaturePurposeName :: SignaturePurpose -> Text -signaturePurposeName RemovalPurpose = "removal" - -signaturePurposeFromName :: Text -> Either String SignaturePurpose -signaturePurposeFromName name = - note ("Unsupported signature purpose " <> T.unpack name) - . getAlt - $ flip foldMap [minBound .. maxBound] - $ \s -> - guard (signaturePurposeName s == name) $> s - -instance FromJSON SignaturePurpose where - parseJSON = - Aeson.withText "SignaturePurpose" $ - either fail pure . signaturePurposeFromName - -instance FromJSONKey SignaturePurpose where - fromJSONKey = - Aeson.FromJSONKeyTextParser $ - either fail pure . signaturePurposeFromName - -instance S.ToParamSchema SignaturePurpose where - toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString - -instance FromHttpApiData SignaturePurpose where - parseQueryParam = first T.pack . signaturePurposeFromName - -instance ToJSON SignaturePurpose where - toJSON = Aeson.String . signaturePurposeName - -instance ToJSONKey SignaturePurpose where - toJSONKey = Aeson.toJSONKeyText signaturePurposeName diff --git a/libs/wire-api/src/Wire/API/MLS/ECDSA.hs b/libs/wire-api/src/Wire/API/MLS/ECDSA.hs new file mode 100644 index 00000000000..11d197c0369 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/ECDSA.hs @@ -0,0 +1,76 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.ECDSA where + +import Crypto.Error +import Crypto.Hash hiding (hash) +import Crypto.PubKey.ECDSA +import Data.ASN1.BinaryEncoding +import Data.ASN1.Encoding +import Data.ASN1.Prim +import Data.Proxy +import Imports +import Wire.API.MLS.Serialisation + +-- | Decode an ECDSA signature. +decodeSignature :: + forall curve. + EllipticCurveECDSA curve => + Proxy curve -> + ByteString -> + Maybe (Signature curve) +decodeSignature curve bs = do + ints <- case decodeASN1' DER bs of + Right ([Start Sequence, IntVal r, IntVal s, End Sequence]) -> pure (r, s) + _ -> Nothing + maybeCryptoError $ signatureFromIntegers curve ints + +-- Encode an ECDSA signature. +encodeSignature :: + forall curve. + EllipticCurveECDSA curve => + Proxy curve -> + Signature curve -> + ByteString +encodeSignature curve sig = case signatureToIntegers curve sig of + (r, s) -> + encodeASN1' + DER + [ Start Sequence, + IntVal r, + IntVal s, + End Sequence + ] + +verifySignature :: + forall curve a hash. + ( EllipticCurveECDSA curve, + HashAlgorithm hash + ) => + Proxy curve -> + hash -> + ByteString -> + RawMLS a -> + ByteString -> + Bool +verifySignature curve hash pub x sig = + fromMaybe False $ do + sig' <- decodeSignature curve sig + pub' <- maybeCryptoError $ decodePublic curve pub + let valid = verify curve hash pub' sig' x.raw + pure valid diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index 179ec9909cd..28a49047332 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -17,52 +15,69 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.MLS.Keys - ( MLSKeys (..), - MLSPublicKeys (..), - mlsKeysToPublic, - ) -where +module Wire.API.MLS.Keys where -import Crypto.PubKey.Ed25519 +import Crypto.ECC (Curve_P256R1, Curve_P384R1, Curve_P521R1) +import Crypto.PubKey.ECDSA qualified as ECDSA import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.ByteArray import Data.Json.Util -import Data.Map qualified as Map import Data.OpenApi qualified as S -import Data.Schema -import Imports +import Data.Proxy +import Data.Schema hiding (HasField) +import Imports hiding (First, getFirst) import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Credential -data MLSKeys = MLSKeys - { mlsKeyPair_ed25519 :: Maybe (SecretKey, PublicKey) +data MLSKeysByPurpose a = MLSKeysByPurpose + { removal :: a } + deriving (Eq, Show, Functor, Foldable, Traversable) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema (MLSKeysByPurpose a) -instance Semigroup MLSKeys where - MLSKeys Nothing <> MLSKeys ed2 = MLSKeys ed2 - MLSKeys ed1 <> MLSKeys _ = MLSKeys ed1 - -instance Monoid MLSKeys where - mempty = MLSKeys Nothing +instance ToSchema a => ToSchema (MLSKeysByPurpose a) where + schema = + object "MLSKeysByPurpose" $ + MLSKeysByPurpose + <$> (.removal) .= field "removal" schema -newtype MLSPublicKeys = MLSPublicKeys - { unMLSPublicKeys :: Map SignaturePurpose (Map SignatureSchemeTag ByteString) +data MLSKeys a = MLSKeys + { ed25519 :: a, + ecdsa_secp256r1_sha256 :: a, + ecdsa_secp384r1_sha384 :: a, + ecdsa_secp521r1_sha512 :: a } - deriving (FromJSON, ToJSON, S.ToSchema) via Schema MLSPublicKeys - deriving newtype (Semigroup, Monoid) + deriving (Eq, Show) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema (MLSKeys a) -instance ToSchema MLSPublicKeys where +instance ToSchema a => ToSchema (MLSKeys a) where schema = - named "MLSKeys" $ - MLSPublicKeys - <$> unMLSPublicKeys - .= map_ (map_ base64Schema) + object "MLSKeys" $ + MLSKeys + <$> ed25519 .= field "ed25519" schema + <*> ecdsa_secp256r1_sha256 .= field "ecdsa_secp256r1_sha256" schema + <*> ecdsa_secp384r1_sha384 .= field "ecdsa_secp384r1_sha384" schema + <*> ecdsa_secp521r1_sha512 .= field "ecdsa_secp521r1_sha512" schema + +data MLSPrivateKeys = MLSPrivateKeys + { mlsKeyPair_ed25519 :: KeyPair Ed25519, + mlsKeyPair_ecdsa_secp256r1_sha256 :: KeyPair Ecdsa_secp256r1_sha256, + mlsKeyPair_ecdsa_secp384r1_sha384 :: KeyPair Ecdsa_secp384r1_sha384, + mlsKeyPair_ecdsa_secp521r1_sha512 :: KeyPair Ecdsa_secp521r1_sha512 + } + +type MLSPublicKeys = MLSKeys MLSPublicKey + +newtype MLSPublicKey = MLSPublicKey {unwrapMLSPublicKey :: ByteString} + deriving (Eq, Show) -mlsKeysToPublic1 :: MLSKeys -> Map SignatureSchemeTag ByteString -mlsKeysToPublic1 (MLSKeys mEd25519key) = - foldMap (Map.singleton Ed25519 . convert . snd) mEd25519key +instance ToSchema MLSPublicKey where + schema = named "MLSPublicKey" $ MLSPublicKey <$> unwrapMLSPublicKey .= base64Schema -mlsKeysToPublic :: (SignaturePurpose -> MLSKeys) -> MLSPublicKeys -mlsKeysToPublic f = flip foldMap [minBound .. maxBound] $ \purpose -> - MLSPublicKeys (Map.singleton purpose (mlsKeysToPublic1 (f purpose))) +mlsKeysToPublic :: MLSPrivateKeys -> MLSPublicKeys +mlsKeysToPublic (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = + MLSKeys + { ed25519 = MLSPublicKey $ convert ed, + ecdsa_secp256r1_sha256 = MLSPublicKey $ ECDSA.encodePublic (Proxy @Curve_P256R1) ec256, + ecdsa_secp384r1_sha384 = MLSPublicKey $ ECDSA.encodePublic (Proxy @Curve_P384R1) ec384, + ecdsa_secp521r1_sha512 = MLSPublicKey $ ECDSA.encodePublic (Proxy @Curve_P521R1) ec521 + } diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs index 2f98d969426..3e4ebe99d9c 100644 --- a/libs/wire-api/src/Wire/API/MLS/Validation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -25,6 +25,7 @@ where import Control.Applicative import Control.Error.Util import Data.ByteArray qualified as BA +import Data.Text qualified as T import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LT import Data.Text.Lazy.Builder.Int qualified as LT @@ -119,9 +120,24 @@ validateCredential cs pkey mIdentity cred = do "Failed to parse identity: " <> e validateCredentialKey :: SignatureSchemeTag -> ByteString -> X509.PubKey -> Either Text () -validateCredentialKey Ed25519 pk1 (X509.PubKeyEd25519 pk2) = - note "Certificate public key does not match client's" $ guard (pk1 == BA.convert pk2) -validateCredentialKey _ _ _ = Left "Certificate signature scheme does not match client's public key" +validateCredentialKey Ed25519 pk1 (X509.PubKeyEd25519 pk2) = validateCredentialKeyBS pk1 (BA.convert pk2) +validateCredentialKey Ecdsa_secp256r1_sha256 pk1 (X509.PubKeyEC pk2) = + case pk2.pubkeyEC_pub of + X509.SerializedPoint bs -> validateCredentialKeyBS pk1 bs +validateCredentialKey Ecdsa_secp384r1_sha384 pk1 (X509.PubKeyEC pk2) = + case pk2.pubkeyEC_pub of + X509.SerializedPoint bs -> validateCredentialKeyBS pk1 bs +validateCredentialKey Ecdsa_secp521r1_sha512 pk1 (X509.PubKeyEC pk2) = + case pk2.pubkeyEC_pub of + X509.SerializedPoint bs -> validateCredentialKeyBS pk1 bs +validateCredentialKey ss _ _ = + Left $ + "Certificate signature scheme " <> T.pack (show ss) <> " does not match client's public key" + +validateCredentialKeyBS :: ByteString -> ByteString -> Either Text () +validateCredentialKeyBS pk1 pk2 = + note "Certificate public key does not match client's" $ + guard (pk1 == pk2) validateSource :: LeafNodeSourceTag -> LeafNodeSource -> Either Text () validateSource t s = do diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 6266979e8c3..1f4340bb372 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -116,7 +116,7 @@ type MLSMessagingAPI = :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" MLSPublicKeys) + :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" (MLSKeysByPurpose MLSPublicKeys)) ) type MLSAPI = LiftNamed ("mls" :> MLSMessagingAPI) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index d0e892bf700..6c6c4fd5abc 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -38,6 +38,7 @@ import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById +import Test.Wire.API.Golden.Manual.MLSKeys import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact import Test.Wire.API.Golden.Manual.SubConversation @@ -193,5 +194,13 @@ tests = [ (testObject_ConversationRemoveMembers_1, "testObject_ConversationRemoveMembers_1.json"), (testObject_ConversationRemoveMembers_2, "testObject_ConversationRemoveMembers_2.json"), (testObject_ConversationRemoveMembers_3, "testObject_ConversationRemoveMembers_3.json") + ], + testGroup "MLSPublicKeys" $ + testObjects + [ (testObject_MLSPublicKeys1, "testObject_MLSPublicKeys_1.json") + ], + testGroup "MLSKeysByPurpose" $ + testObjects + [ (testObject_MLSKeysByPurpose1, "testObject_MLSKeysByPurpose_1.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/MLSKeys.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/MLSKeys.hs new file mode 100644 index 00000000000..bd12d7f7af2 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/MLSKeys.hs @@ -0,0 +1,44 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.MLSKeys where + +import Data.Json.Util +import Wire.API.MLS.Keys + +testObject_MLSPublicKeys1 :: MLSPublicKeys +testObject_MLSPublicKeys1 = + MLSKeys + { ed25519 = + MLSPublicKey + (fromBase64TextLenient "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw="), + ecdsa_secp256r1_sha256 = + MLSPublicKey + (fromBase64TextLenient "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM="), + ecdsa_secp384r1_sha384 = + MLSPublicKey + (fromBase64TextLenient "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV"), + ecdsa_secp521r1_sha512 = + MLSPublicKey + (fromBase64TextLenient "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==") + } + +testObject_MLSKeysByPurpose1 :: MLSKeysByPurpose MLSPublicKeys +testObject_MLSKeysByPurpose1 = + MLSKeysByPurpose + { removal = testObject_MLSPublicKeys1 + } diff --git a/libs/wire-api/test/golden/testObject_MLSKeysByPurpose_1.json b/libs/wire-api/test/golden/testObject_MLSKeysByPurpose_1.json new file mode 100644 index 00000000000..ec9dbea6c97 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_MLSKeysByPurpose_1.json @@ -0,0 +1,8 @@ +{ + "removal": { + "ecdsa_secp256r1_sha256": "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM=", + "ecdsa_secp384r1_sha384": "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV", + "ecdsa_secp521r1_sha512": "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==", + "ed25519": "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw=" + } +} diff --git a/libs/wire-api/test/golden/testObject_MLSPublicKeys_1.json b/libs/wire-api/test/golden/testObject_MLSPublicKeys_1.json new file mode 100644 index 00000000000..2ff1863226c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_MLSPublicKeys_1.json @@ -0,0 +1,6 @@ +{ + "ecdsa_secp256r1_sha256": "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM=", + "ecdsa_secp384r1_sha384": "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV", + "ecdsa_secp521r1_sha512": "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==", + "ed25519": "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw=" +} diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs index b1f51f3b259..e98ae87e01f 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs @@ -236,15 +236,15 @@ testRemoveProposalMessageSignature = withSystemTempDirectory "mls" $ \tmp -> do void $ spawn (cli qcid tmp ["member", "add", "--group", tmp groupFilename, "--in-place", tmp qcid2]) Nothing let proposal = mkRawMLS (RemoveProposal 1) - pmessage = - mkSignedPublicMessage - secretKey - publicKey - gid - (Epoch 1) - (TaggedSenderExternal 0) - (FramedContentProposal proposal) - message = mkMessage $ MessagePublic pmessage + pmessage <- + mkSignedPublicMessage + @Ed25519 + (secretKey, publicKey) + gid + (Epoch 1) + (TaggedSenderExternal 0) + (FramedContentProposal proposal) + let message = mkMessage $ MessagePublic pmessage messageFilename = "signed-message.mls" BS.writeFile (tmp messageFilename) (raw (mkRawMLS message)) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 91dc8a3af64..31007dca783 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -109,6 +109,7 @@ library Wire.API.MLS.CommitBundle Wire.API.MLS.Context Wire.API.MLS.Credential + Wire.API.MLS.ECDSA Wire.API.MLS.Epoch Wire.API.MLS.Extension Wire.API.MLS.Group @@ -241,6 +242,7 @@ library hs-source-dirs: src build-depends: , aeson >=2.0.1.0 + , asn1-encoding , async , attoparsec >=0.10 , base >=4 && <5 @@ -581,6 +583,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.GroupId Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.ListUsersById + Test.Wire.API.Golden.Manual.MLSKeys Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap Test.Wire.API.Golden.Manual.SearchResultContact Test.Wire.API.Golden.Manual.SubConversation diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 65166a8db8d..115f08511b5 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -7,18 +7,17 @@ rustPlatform.buildRustPackage rec { src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - rev = "a18470061977211ecf81911de0f2632eefc81efd"; - sha256 = "sha256-HgwR6vuVL3eN7NVox+iClPxqsat3Znc9+ZtENuEJKSU="; + rev = "0b7bad3a5021d069bcf02aa0d0a3fe0a6fdabe72"; + sha256 = "sha256-bFNqDG2UhN8kOEdGFdhPHN/Wz1y67Wcp1c/z0f0vHfE="; }; pname = "mls-test-cli"; - version = "0.10.3"; + version = "0.11"; cargoLock = { lockFile = "${src}/Cargo.lock"; outputHashes = { "hpke-0.10.0" = "sha256-T1+BFwX6allljNZ/8T3mrWhOejnUU27BiWQetqU+0fY="; - "openmls-1.0.0" = "sha256-nyIMAlTy7CTV0bVQ0ytamKHpERgtsVKTX4zv7aHzemo="; + "openmls-1.0.0" = "sha256-MOf6F6jy2ofZ05leN9npDAlxYkn2S+hVOq/MSlKWBiU="; "safe_pqc_kyber-0.6.2" = "sha256-9t+IIohCJcMIWRtqLA0idyMmjev82BtpST15Tthlge4="; - "tls_codec-0.3.0" = "sha256-IO6tenXKkC14EoUDp/+DtFNOVzDfOlLu8K1EJI7sOzs="; }; }; doCheck = false; diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 15146fc480e..acf9326915f 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -50,6 +50,9 @@ settings: mlsPrivateKeyPaths: removal: ed25519: test/resources/ed25519.pem + ecdsa_secp256r1_sha256: test/resources/ecdsa_secp256r1_sha256.pem + ecdsa_secp384r1_sha384: test/resources/ecdsa_secp384r1_sha384.pem + ecdsa_secp521r1_sha512: test/resources/ecdsa_secp521r1_sha512.pem guestLinkTTLSeconds: 604800 # We explicitly do not disable any API version. Please make sure the configuration value is the same in all these configs: # brig, cannon, cargohold, galley, gundeck, proxy, spar. diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index eca0c29b37d..f6cbd33f32b 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -153,6 +153,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member LegalHoldStore r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TeamStore r, Member TinyLog r, @@ -170,6 +171,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (Input Env) r, Member ProposalStore r, Member SubConversationStore r, + Member Random r, Member TinyLog r ) HasConversationActionEffects 'ConversationRemoveMembersTag r = @@ -183,6 +185,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member FederatorAccess r, Member GundeckAccess r, Member (Error InternalError) r, + Member Random r, Member TinyLog r, Member (Error NoChanges) r ) @@ -228,7 +231,8 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member TinyLog r, Member (Input UTCTime) r, Member ConversationStore r, - Member SubConversationStore r + Member SubConversationStore r, + Member Random r ) HasConversationActionEffects 'ConversationMessageTimerUpdateTag r = ( Member ConversationStore r, @@ -255,6 +259,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, Member TeamStore r, @@ -1039,7 +1044,8 @@ kickMember :: Member (Input Env) r, Member MemberStore r, Member SubConversationStore r, - Member TinyLog r + Member TinyLog r, + Member Random r ) => Qualified UserId -> Local Conversation -> diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index 35517d5ec14..b3dd2a8c3f4 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -106,6 +106,7 @@ rmClientH :: Member MemberStore r, Member (Error InternalError) r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member P.TinyLog r ) => diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 377b17efc99..79a7069f551 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -44,7 +44,6 @@ import Data.UUID.Tagged qualified as U import Galley.API.Action import Galley.API.Error import Galley.API.MLS -import Galley.API.MLS.Keys (getMLSRemovalKey) import Galley.API.Mapping import Galley.API.One2One import Galley.API.Util @@ -206,8 +205,6 @@ createGroupConversationGeneric lusr conn newConv = do when (newConvProtocol newConv == BaseProtocolMLSTag) $ do -- Here we fail early in order to notify users of this misconfiguration assertMLSEnabled - unlessM (isJust <$> getMLSRemovalKey) $ - throw (InternalErrorWithDescription "No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Refusing to create MLS conversation.") lcnv <- traverse (const E.createConversationId) lusr do diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 4bce0364522..caba6054e6e 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -136,6 +136,7 @@ onClientRemoved :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -256,6 +257,7 @@ leaveConversation :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -404,6 +406,7 @@ onUserDeleted :: Member (Input Env) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -471,6 +474,7 @@ updateConversation :: Member TeamStore r, Member TinyLog r, Member ConversationStore r, + Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, Member (Input (Local ())) r @@ -590,6 +594,7 @@ sendMLSCommitBundle :: Member Resource r, Member TeamStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member ProposalStore r ) => diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 3c5dc23cbbf..2d64d33842c 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -318,6 +318,7 @@ rmUser :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, Member TeamStore r diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 5c23f29b89d..54fe4ae2b6d 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -164,6 +164,7 @@ removeSettingsInternalPaging :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, Member (TeamMemberStore InternalPaging) r, @@ -208,6 +209,7 @@ removeSettings :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r ) => UserId -> @@ -261,6 +263,7 @@ removeSettings' :: Member (TeamMemberStore p) r, Member TeamStore r, Member ProposalStore r, + Member Random r, Member P.TinyLog r, Member SubConversationStore r ) => @@ -347,6 +350,7 @@ grantConsent :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamStore r ) => @@ -394,6 +398,7 @@ requestDevice :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, Member TeamStore r @@ -474,6 +479,7 @@ approveDevice :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, Member TeamStore r @@ -550,6 +556,7 @@ disableForUser :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamStore r ) => @@ -604,6 +611,7 @@ changeLegalholdStatus :: Member MemberStore r, Member TeamStore r, Member ProposalStore r, + Member Random r, Member P.TinyLog r, Member SubConversationStore r ) => @@ -719,6 +727,7 @@ handleGroupConvPolicyConflicts :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, + Member Random r, Member SubConversationStore r, Member TeamStore r ) => diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index 2b06791739d..2b8c0b6fd67 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -25,7 +25,6 @@ module Galley.API.MLS ) where -import Control.Lens (view) import Data.Id import Data.Qualified import Galley.API.MLS.Enabled @@ -43,8 +42,6 @@ getMLSPublicKeys :: Member (ErrorS 'MLSNotEnabled) r ) => Local UserId -> - Sem r MLSPublicKeys + Sem r (MLSKeysByPurpose MLSPublicKeys) getMLSPublicKeys _ = do - assertMLSEnabled - keys <- inputs (view mlsKeys) - pure $ mlsKeysToPublic keys + fmap mlsKeysToPublic <$> getMLSPrivateKeys diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index 59b3e7cd394..9eba2544648 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -87,7 +87,8 @@ type HasProposalActionEffects r = Member ProposalStore r, Member SubConversationStore r, Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member Random r ) getCommitData :: diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index 8525429d944..8a1bbe7fe21 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -70,7 +70,8 @@ processInternalCommit :: Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MissingLegalholdConsent) r, Member SubConversationStore r, - Member Resource r + Member Resource r, + Member Random r ) => ClientIdentity -> Maybe ConnId -> diff --git a/services/galley/src/Galley/API/MLS/Enabled.hs b/services/galley/src/Galley/API/MLS/Enabled.hs index 1af66279a2a..d8106726f0f 100644 --- a/services/galley/src/Galley/API/MLS/Enabled.hs +++ b/services/galley/src/Galley/API/MLS/Enabled.hs @@ -15,22 +15,19 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.API.MLS.Enabled - ( isMLSEnabled, - assertMLSEnabled, - ) -where +module Galley.API.MLS.Enabled where -import Galley.API.MLS.Keys +import Control.Lens (view) import Galley.Env -import Imports +import Imports hiding (getFirst) import Polysemy import Polysemy.Input import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.MLS.Keys isMLSEnabled :: Member (Input Env) r => Sem r Bool -isMLSEnabled = isJust <$> getMLSRemovalKey +isMLSEnabled = inputs (isJust . view mlsKeys) -- | Fail if MLS is not enabled. Only use this function at the beginning of an -- MLS endpoint, NOT in utility functions. @@ -39,6 +36,11 @@ assertMLSEnabled :: Member (ErrorS 'MLSNotEnabled) r ) => Sem r () -assertMLSEnabled = - unlessM isMLSEnabled $ - throwS @'MLSNotEnabled +assertMLSEnabled = void getMLSPrivateKeys + +getMLSPrivateKeys :: + ( Member (Input Env) r, + Member (ErrorS 'MLSNotEnabled) r + ) => + Sem r (MLSKeysByPurpose MLSPrivateKeys) +getMLSPrivateKeys = noteS @'MLSNotEnabled =<< inputs (view mlsKeys) diff --git a/services/galley/src/Galley/API/MLS/Keys.hs b/services/galley/src/Galley/API/MLS/Keys.hs index 3db1ebfd9c3..f8bfe8e458b 100644 --- a/services/galley/src/Galley/API/MLS/Keys.hs +++ b/services/galley/src/Galley/API/MLS/Keys.hs @@ -15,16 +15,43 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.API.MLS.Keys (getMLSRemovalKey) where +module Galley.API.MLS.Keys (getMLSRemovalKey, SomeKeyPair (..)) where +import Control.Error.Util (hush) import Control.Lens (view) -import Crypto.PubKey.Ed25519 (PublicKey, SecretKey) +import Data.Proxy import Galley.Env -import Imports +import Imports hiding (getFirst) import Polysemy +import Polysemy.Error import Polysemy.Input -import Wire.API.MLS.Credential (SignaturePurpose (RemovalPurpose)) +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Keys -getMLSRemovalKey :: Member (Input Env) r => Sem r (Maybe (SecretKey, PublicKey)) -getMLSRemovalKey = mlsKeyPair_ed25519 <$> (inputs (view mlsKeys) <*> pure RemovalPurpose) +data SomeKeyPair where + SomeKeyPair :: forall ss. IsSignatureScheme ss => Proxy ss -> KeyPair ss -> SomeKeyPair + +getMLSRemovalKey :: + Member (Input Env) r => + SignatureSchemeTag -> + Sem r (Maybe SomeKeyPair) +getMLSRemovalKey ss = fmap hush . runError @() $ do + keysByPurpose <- note () =<< inputs (view mlsKeys) + let keys = keysByPurpose.removal + case ss of + Ed25519 -> pure $ SomeKeyPair (Proxy @Ed25519) (mlsKeyPair_ed25519 keys) + Ecdsa_secp256r1_sha256 -> + pure $ + SomeKeyPair + (Proxy @Ecdsa_secp256r1_sha256) + (mlsKeyPair_ecdsa_secp256r1_sha256 keys) + Ecdsa_secp384r1_sha384 -> + pure $ + SomeKeyPair + (Proxy @Ecdsa_secp384r1_sha384) + (mlsKeyPair_ecdsa_secp384r1_sha384 keys) + Ecdsa_secp521r1_sha512 -> + pure $ + SomeKeyPair + (Proxy @Ecdsa_secp521r1_sha512) + (mlsKeyPair_ecdsa_secp521r1_sha512 keys) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 90f0c3c0ad3..8dfdb49497f 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -147,6 +147,7 @@ postMLSMessageFromLocalUser lusr c conn smsg = do postMLSCommitBundle :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, + Member Random r, Member Resource r, Member SubConversationStore r ) => @@ -168,6 +169,7 @@ postMLSCommitBundle loc qusr c ctype qConvOrSub conn bundle = postMLSCommitBundleFromLocalUser :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, + Member Random r, Member Resource r, Member SubConversationStore r ) => @@ -190,7 +192,8 @@ postMLSCommitBundleToLocalConv :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, Member Resource r, - Member SubConversationStore r + Member SubConversationStore r, + Member Random r ) => Qualified UserId -> ClientId -> diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 4e2548fead2..4dcf36073cb 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -27,11 +27,12 @@ where import Data.Bifunctor import Data.Id import Data.Map qualified as Map +import Data.Proxy import Data.Qualified import Data.Set qualified as Set import Data.Time import Galley.API.MLS.Conversation -import Galley.API.MLS.Keys (getMLSRemovalKey) +import Galley.API.MLS.Keys import Galley.API.MLS.Propagate import Galley.API.MLS.Types import Galley.Data.Conversation.Types @@ -51,15 +52,18 @@ import System.Logger qualified as Log import Wire.API.Conversation.Protocol import Wire.API.Federation.Error import Wire.API.MLS.AuthenticatedContent +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.LeafNode import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation +import Wire.Sem.Random -- | Send remove proposals for a set of clients to clients in the ClientMap. createAndSendRemoveProposals :: + forall r t. ( Member (Error FederationError) r, Member (Input UTCTime) r, Member TinyLog r, @@ -68,6 +72,7 @@ createAndSendRemoveProposals :: Member GundeckAccess r, Member ProposalStore r, Member (Input Env) r, + Member Random r, Foldable t ) => Local ConvOrSubConv -> @@ -83,22 +88,22 @@ createAndSendRemoveProposals :: Sem r () createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do let meta = (tUnqualified lConvOrSubConv).mlsMeta - mKeyPair <- getMLSRemovalKey + mKeyPair <- getMLSRemovalKey (csSignatureScheme (cnvmlsCipherSuite meta)) case mKeyPair of Nothing -> do warn $ Log.msg ("No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Not able to remove client from MLS conversation." :: Text) - Just (secKey, pubKey) -> do + Just (SomeKeyPair (_ :: Proxy ss) kp) -> do for_ indices $ \idx -> do let proposal = mkRawMLS (RemoveProposal idx) - pmsg = - mkSignedPublicMessage - secKey - pubKey - (cnvmlsGroupId meta) - (cnvmlsEpoch meta) - (TaggedSenderExternal 0) - (FramedContentProposal proposal) - msg = mkRawMLS (mkMessage (MessagePublic pmsg)) + pmsg <- + liftRandom $ + mkSignedPublicMessage @ss + kp + (cnvmlsGroupId meta) + (cnvmlsEpoch meta) + (TaggedSenderExternal 0) + (FramedContentProposal proposal) + let msg = mkRawMLS (mkMessage (MessagePublic pmsg)) storeProposal (cnvmlsGroupId meta) (cnvmlsEpoch meta) @@ -118,6 +123,7 @@ removeClientsWithClientMapRecursively :: Member ProposalStore r, Member SubConversationStore r, Member (Input Env) r, + Member Random r, Functor f, Foldable f ) => @@ -151,6 +157,7 @@ removeClientsFromSubConvs :: Member ProposalStore r, Member SubConversationStore r, Member (Input Env) r, + Member Random r, Functor f, Foldable f ) => @@ -188,6 +195,7 @@ removeClient :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -224,6 +232,7 @@ removeUser :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -270,6 +279,7 @@ removeExtraneousClients :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 3f81288a957..d9d267797fa 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -334,6 +334,7 @@ type HasLeaveSubConversationEffects r = Input UTCTime, MemberStore, ProposalStore, + Random, SubConversationStore, TinyLog ] diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index aaa01a9daa4..a06730e5cfd 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -58,7 +58,6 @@ import Data.Range import Data.Set qualified as Set import Galley.API.Error import Galley.API.MLS -import Galley.API.MLS.Keys import Galley.API.MLS.One2One import Galley.API.MLS.Types import Galley.API.Mapping @@ -437,9 +436,7 @@ conversationIdsPageFrom lusr state = do -- backend removal key is a proxy for it) the self-conversation is not -- returned or attempted to be created; in that case we skip anything related -- to it. - whenM (isJust <$> getMLSRemovalKey) - . void - $ getMLSSelfConversation lusr + whenM isMLSEnabled $ void $ getMLSSelfConversation lusr conversationIdsPageFromV2 ListGlobalSelf lusr state getConversations :: diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index f18b1fe6c59..7a9960608cf 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -317,7 +317,8 @@ instance SetFeatureConfig LegalholdConfig where Member TeamFeatureStore r, Member TeamStore r, Member (TeamMemberStore InternalPaging) r, - Member P.TinyLog r + Member P.TinyLog r, + Member Random r ) -- we're good to update the status now. diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index b6982b8c5f0..6d6ec50ec6b 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -290,6 +290,7 @@ type UpdateConversationAccessEffects = Input UTCTime, MemberStore, ProposalStore, + Random, SubConversationStore, TeamStore, TinyLog @@ -718,6 +719,7 @@ updateConversationProtocolWithLocalUser :: Member GundeckAccess r, Member ExternalAccess r, Member FederatorAccess r, + Member Random r, Member ProposalStore r, Member SubConversationStore r, Member TeamFeatureStore r, @@ -874,6 +876,7 @@ addMembers :: Member LegalHoldStore r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TeamStore r, Member TinyLog r @@ -915,6 +918,7 @@ addMembersUnqualifiedV2 :: Member LegalHoldStore r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TeamStore r, Member TinyLog r @@ -956,6 +960,7 @@ addMembersUnqualified :: Member LegalHoldStore r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TeamStore r, Member TinyLog r @@ -1133,6 +1138,7 @@ removeMemberUnqualified :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -1161,6 +1167,7 @@ removeMemberQualified :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => @@ -1236,6 +1243,7 @@ removeMemberFromLocalConv :: Member (Input UTCTime) r, Member MemberStore r, Member ProposalStore r, + Member Random r, Member SubConversationStore r, Member TinyLog r ) => diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 02c65fd33e9..a934a73c497 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -162,7 +162,7 @@ createEnv m o l = do <$> Q.new 16000 <*> pure initExtEnv <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) - <*> loadAllMLSKeys (fold (o ^. settings . mlsPrivateKeyPaths)) + <*> traverse loadAllMLSKeys (o ^. settings . mlsPrivateKeyPaths) <*> traverse (mkRabbitMqChannelMVar l) (o ^. rabbitmq) <*> pure codeURIcfg diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 12c7c31df5f..ce9abba6671 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -43,6 +43,7 @@ module Galley.Effects SearchVisibilityStore, ServiceStore, SubConversationStore, + Random, TeamFeatureStore, TeamMemberStore, TeamNotificationStore, diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 4a9687c3e3d..87e189e2c29 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -39,7 +39,6 @@ import OpenSSL.Session as Ssl import Ssl.Util import System.Logger import Util.Options -import Wire.API.MLS.Credential import Wire.API.MLS.Keys import Wire.API.Team.Member @@ -60,7 +59,7 @@ data Env = Env _deleteQueue :: Q.Queue DeleteItem, _extGetManager :: [Fingerprint Rsa] -> IO Manager, _aEnv :: Maybe Aws.Env, - _mlsKeys :: SignaturePurpose -> MLSKeys, + _mlsKeys :: Maybe (MLSKeysByPurpose MLSPrivateKeys), _rabbitmqChannel :: Maybe (MVar Q.Channel), _convCodeURI :: Either HttpsUrl (Map Text HttpsUrl) } diff --git a/services/galley/src/Galley/Keys.hs b/services/galley/src/Galley/Keys.hs index 7614ed9fc9f..5cbfa540b21 100644 --- a/services/galley/src/Galley/Keys.hs +++ b/services/galley/src/Galley/Keys.hs @@ -22,22 +22,26 @@ module Galley.Keys ) where +import Control.Error.Util import Control.Exception -import Crypto.PubKey.Ed25519 +import Crypto.ECC hiding (KeyPair) +import Crypto.Error +import Crypto.PubKey.ECDSA qualified as ECDSA +import Crypto.PubKey.Ed25519 qualified as Ed25519 import Data.ASN1.BinaryEncoding +import Data.ASN1.BitArray import Data.ASN1.Encoding import Data.ASN1.Types import Data.Bifunctor import Data.ByteString.Lazy qualified as LBS -import Data.Map qualified as Map import Data.PEM +import Data.Proxy import Data.X509 import Imports import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Credential import Wire.API.MLS.Keys -type MLSPrivateKeyPaths = Map SignaturePurpose (Map SignatureSchemeTag FilePath) +type MLSPrivateKeyPaths = MLSKeysByPurpose (MLSKeys FilePath) data MLSPrivateKeyException = MLSPrivateKeyException { mpkePath :: FilePath, @@ -48,28 +52,115 @@ data MLSPrivateKeyException = MLSPrivateKeyException instance Exception MLSPrivateKeyException where displayException e = mpkePath e <> ": " <> mpkeMsg e -mapToFunction :: (Ord k, Monoid m) => Map k m -> k -> m -mapToFunction m x = Map.findWithDefault mempty x m +loadAllMLSKeys :: MLSPrivateKeyPaths -> IO (MLSKeysByPurpose MLSPrivateKeys) +loadAllMLSKeys = traverse loadMLSKeys -loadAllMLSKeys :: MLSPrivateKeyPaths -> IO (SignaturePurpose -> MLSKeys) -loadAllMLSKeys = fmap mapToFunction . traverse loadMLSKeys +loadMLSKeys :: MLSKeys FilePath -> IO MLSPrivateKeys +loadMLSKeys paths = + MLSPrivateKeys + <$> loadKeyPair @Ed25519 paths.ed25519 + <*> loadKeyPair @Ecdsa_secp256r1_sha256 paths.ecdsa_secp256r1_sha256 + <*> loadKeyPair @Ecdsa_secp384r1_sha384 paths.ecdsa_secp384r1_sha384 + <*> loadKeyPair @Ecdsa_secp521r1_sha512 paths.ecdsa_secp521r1_sha512 -loadMLSKeys :: Map SignatureSchemeTag FilePath -> IO MLSKeys -loadMLSKeys m = - MLSKeys - <$> traverse loadEd25519KeyPair (Map.lookup Ed25519 m) +class LoadKeyPair (ss :: SignatureSchemeTag) where + loadKeyPair :: FilePath -> IO (KeyPair ss) -loadEd25519KeyPair :: FilePath -> IO (SecretKey, PublicKey) +instance LoadKeyPair Ed25519 where + loadKeyPair = loadEd25519KeyPair + +instance LoadKeyPair Ecdsa_secp256r1_sha256 where + loadKeyPair = loadECDSAKeyPair @Curve_P256R1 + +instance LoadKeyPair Ecdsa_secp384r1_sha384 where + loadKeyPair = loadECDSAKeyPair @Curve_P384R1 + +instance LoadKeyPair Ecdsa_secp521r1_sha512 where + loadKeyPair = loadECDSAKeyPair @Curve_P521R1 + +class CurveOID c where + curveOID :: [Integer] + +instance CurveOID Curve_P256R1 where + curveOID = [1, 2, 840, 10045, 3, 1, 7] + +instance CurveOID Curve_P384R1 where + curveOID = [1, 3, 132, 0, 34] + +instance CurveOID Curve_P521R1 where + curveOID = [1, 3, 132, 0, 35] + +loadECDSAKeyPair :: + forall c. + (ECDSA.EllipticCurveECDSA c, CurveOID c) => + FilePath -> + IO (ECDSA.PrivateKey c, ECDSA.PublicKey c) +loadECDSAKeyPair path = do + bytes <- LBS.readFile path + either (throwIO . MLSPrivateKeyException path) pure $ + decodeEcdsaKeyPair @c bytes + +loadEd25519KeyPair :: FilePath -> IO (Ed25519.SecretKey, Ed25519.PublicKey) loadEd25519KeyPair path = do bytes <- LBS.readFile path priv <- either (throwIO . MLSPrivateKeyException path) pure $ decodeEd25519PrivateKey bytes - pure (priv, toPublic priv) + pure (priv, Ed25519.toPublic priv) + +decodeEcdsaKeyPair :: + forall c. + (ECDSA.EllipticCurveECDSA c, CurveOID c) => + LByteString -> + Either String (ECDSA.PrivateKey c, ECDSA.PublicKey c) +decodeEcdsaKeyPair bytes = do + let curve = Proxy @c + pems <- pemParseLBS bytes + pem <- expectOne "private key" pems + let content = pemContent pem + -- parse outer pkcs8 container as BER + asn1 <- first displayException (decodeASN1' BER content) + (oid, key) <- case asn1 of + [ Start Sequence, + IntVal _version, + Start Sequence, + OID [1, 2, 840, 10045, 2, 1], -- ecdsa + OID oid, + End Sequence, + OctetString key, + End Sequence + ] -> pure (oid, key) + _ -> Left "invalid ECDSA key format: expected pkcs8" + note + ( "private key curve mismatch, expected " + <> show (curveOID @c) + <> ", found " + <> show oid + ) + $ guard (oid == curveOID @c) + -- parse key bytestring as BER again, this should be in the format of rfc5915 + asn1' <- first displayException (decodeASN1' BER key) + (privBS, pubBS) <- case asn1' of + [ Start Sequence, + IntVal _version, + OctetString priv, + Start (Container Context _), + BitString (BitArray _ pub), + End (Container Context _), + End Sequence + ] -> pure (priv, pub) + _ -> Left "invalid ECDSA key format: expected rfc5915 private key format" + priv <- + first displayException . eitherCryptoError $ + ECDSA.decodePrivate curve privBS + pub <- + first displayException . eitherCryptoError $ + ECDSA.decodePublic curve pubBS + pure (priv, pub) decodeEd25519PrivateKey :: LByteString -> - Either String SecretKey + Either String Ed25519.SecretKey decodeEd25519PrivateKey bytes = do pems <- pemParseLBS bytes pem <- expectOne "private key" pems @@ -81,11 +172,11 @@ decodeEd25519PrivateKey bytes = do PrivKeyEd25519 sec -> pure sec _ -> Left $ "invalid signature scheme (expected ed25519)" where - expectOne :: String -> [a] -> Either String a - expectOne label [] = Left $ "no " <> label <> " found" - expectOne _ [x] = pure x - expectOne label _ = Left $ "found multiple " <> label <> "s" - expectEmpty :: [a] -> Either String () expectEmpty [] = pure () expectEmpty _ = Left "extraneous ASN.1 data" + +expectOne :: String -> [a] -> Either String a +expectOne label [] = Left $ "no " <> label <> " found" +expectOne _ [x] = pure x +expectOne label _ = Left $ "found multiple " <> label <> "s" diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 30a10f4102e..eea7bdc960e 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -137,11 +137,6 @@ data Settings = Settings -- - wire.com -- - example.com _federationDomain :: !Domain, - -- | When true, galley will assume data in `billing_team_member` table is - -- consistent and use it for billing. - -- When false, billing information for large teams is not guaranteed to have all - -- the owners. - -- Defaults to false. _mlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _featureFlags :: !FeatureFlags, diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index d998d891fc7..9c61172ec0c 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -59,7 +59,6 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential -import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message @@ -153,7 +152,6 @@ tests s = test s "remove users bypassing MLS" testRemoveUsersDirectly, test s "send proteus message to an MLS conversation" testProteusMessage ], - test s "public keys" testPublicKeys, testGroup "GroupInfo" [ test s "get group info for a local conversation" testGetGroupInfoOfLocalConv, @@ -1200,29 +1198,6 @@ testExternalAddProposalWrongUser = do const 404 === statusCode const (Just "no-conversation") === fmap Wai.label . responseJsonError --- FUTUREWORK: test processing a commit containing the external proposal -testPublicKeys :: TestM () -testPublicKeys = do - u <- randomId - g <- viewGalley - keys <- - responseJsonError - =<< get - ( g - . paths ["mls", "public-keys"] - . zUser u - ) - TestM () saveRemovalKey fp = do keys <- fromJust <$> view (tsGConf . settings . mlsPrivateKeyPaths) keysByPurpose <- liftIO $ loadAllMLSKeys keys - let (_, pub) = fromJust (mlsKeyPair_ed25519 (keysByPurpose RemovalPurpose)) - liftIO $ BS.writeFile fp (BA.convert pub) + let pub = (mlsKeysToPublic keysByPurpose.removal).ed25519 + liftIO $ BS.writeFile fp (BA.convert $ unwrapMLSPublicKey pub) data MLSState = MLSState { mlsBaseDir :: FilePath, diff --git a/services/galley/test/resources/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..69450327af3 --- /dev/null +++ b/services/galley/test/resources/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3qjgQ9U+/rTBObn9 +tXSVi2UtHksRDXmQ1VOszFZfjryhRANCAATNkLmZZLyORf5D3PUOxt+rkJTE5vuD +aCqZ7sE5NSN8InRRwuQ1kv0oblDVeQA89ZlHqyxx75JPK+/air7Z1n5I +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..28b7a630d33 --- /dev/null +++ b/services/galley/test/resources/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBLwv3i5LDz9b++O0iw +QAit/Uq7L5PWPgKN99wCm8xkZnuyqWujXW4wvlVUVlZWgh2hZANiAAT0+RXKE31c +VxdYazaVopY50/nV9c18uRdqoENBvtxuD6oDtJtU6oCS/Htkd8JEArTQ9ZHqq144 +yRjuc3d2CqvJmEA/lzIBk9wnz+lghFhvB4TkSHvvLyEBc9DZvhb4EEQ= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..6634ae5251f --- /dev/null +++ b/services/galley/test/resources/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiaEARm5BMaRct1xj +MlemUHijWGAoHtNMhSttSr4jo0WxMwfMnvnDQJSlO2Zs4Tzum2j5eO34EHu6MUrv +qquZYwyhgYkDgYYABAHuvCV/+gJitvAbDwgrBHZJ41oy8Lc+wPIM7Yp6s/vTzTsG +Klo7aMdkx6DUjv/56tVD9bZNulFAjwS8xoIyWg8NSAE1ofo8CBvN1XGZOWuMYjEh +zLrZADduEnOvayw5sEvm135WC0vWjPJaYwKZPdDIXUz9ILJPgNe3gEUvHsDEXvdX +lw== +-----END PRIVATE KEY-----