diff --git a/changelog.d/1-api-changes/mls-private-keys b/changelog.d/1-api-changes/mls-private-keys new file mode 100644 index 00000000000..81df8031270 --- /dev/null +++ b/changelog.d/1-api-changes/mls-private-keys @@ -0,0 +1 @@ +Expose MLS public keys in a new endpoint `GET /mls/public-keys`. diff --git a/changelog.d/6-federation/mls-private-keys b/changelog.d/6-federation/mls-private-keys new file mode 100644 index 00000000000..7f41cffee33 --- /dev/null +++ b/changelog.d/6-federation/mls-private-keys @@ -0,0 +1 @@ +Add mlsPrivateKeyPaths setting to galley diff --git a/charts/galley/templates/secret.yaml b/charts/galley/templates/aws-secret.yaml similarity index 95% rename from charts/galley/templates/secret.yaml rename to charts/galley/templates/aws-secret.yaml index 449be3903f9..a72862ee5a4 100644 --- a/charts/galley/templates/secret.yaml +++ b/charts/galley/templates/aws-secret.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: - name: galley + name: galley-aws labels: app: galley chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 664868ef21c..405f37de74d 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -59,6 +59,11 @@ data: enableIndexedBillingTeamMembers: {{ .settings.enableIndexedBillingTeamMembers }} {{- end }} federationDomain: {{ .settings.federationDomain }} + mlsPrivateKeyPaths: + {{- if $.Values.secrets.mlsPrivateKeys.removal.ed25519 }} + removal: + ed25519: "/etc/wire/galley/secrets/removal_ed25519.pem" + {{- end }} {{- if .settings.featureFlags }} featureFlags: sso: {{ .settings.featureFlags.sso }} diff --git a/charts/galley/templates/deployment.yaml b/charts/galley/templates/deployment.yaml index ca23d999674..c5f1b9ee258 100644 --- a/charts/galley/templates/deployment.yaml +++ b/charts/galley/templates/deployment.yaml @@ -25,13 +25,17 @@ spec: annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/aws-secret: {{ include (print .Template.BasePath "/aws-secret.yaml") . | sha256sum }} + checksum/mls-secret: {{ include (print .Template.BasePath "/mls-secret.yaml") . | sha256sum }} spec: serviceAccountName: {{ .Values.serviceAccount.name }} volumes: - name: "galley-config" configMap: name: "galley" + - name: "galley-secrets" + secret: + secretName: "galley-mls" containers: - name: galley image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -39,17 +43,19 @@ spec: volumeMounts: - name: "galley-config" mountPath: "/etc/wire/galley/conf" + - name: "galley-secrets" + mountPath: "/etc/wire/galley/secrets" env: {{- if hasKey .Values.secrets "awsKeyId" }} - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: - name: galley + name: galley-aws key: awsKeyId - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: - name: galley + name: galley-aws key: awsSecretKey {{- end }} - name: AWS_REGION diff --git a/charts/galley/templates/mls-secret.yaml b/charts/galley/templates/mls-secret.yaml new file mode 100644 index 00000000000..7f03f292d8b --- /dev/null +++ b/charts/galley/templates/mls-secret.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Secret +metadata: + name: galley-mls + labels: + app: galley + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- if .Values.secrets.mlsPrivateKeys.removal.ed25519 }} + removal_ed25519.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ed25519 | b64enc | quote }} + {{- end -}} diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/galley/templates/tests/galley-integration.yaml index a688764dfe4..883f57e2d9d 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -36,6 +36,9 @@ spec: - name: "galley-integration-secrets" configMap: name: "galley-integration-secrets" + - name: "galley-secrets" + secret: + secretName: "galley-mls" containers: - name: integration image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" @@ -47,6 +50,8 @@ spec: - name: "galley-integration-secrets" # TODO: see corresp. TODO in brig. mountPath: "/etc/wire/integration-secrets" + - name: "galley-secrets" + mountPath: "/etc/wire/galley/secrets" env: # these dummy values are necessary for Amazonka's "Discover" - name: AWS_ACCESS_KEY_ID diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 76406d88aab..e62f224a8f1 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -92,4 +92,4 @@ serviceAccount: annotations: {} automountServiceAccountToken: true -secrets: {} +secrets: diff --git a/deploy/services-demo/conf/ed25519.pem b/deploy/services-demo/conf/ed25519.pem new file mode 100644 index 00000000000..4e87cf573cf --- /dev/null +++ b/deploy/services-demo/conf/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c +-----END PRIVATE KEY----- diff --git a/deploy/services-demo/conf/galley.demo.yaml b/deploy/services-demo/conf/galley.demo.yaml index 628e0d22b22..9e9150ce5ca 100644 --- a/deploy/services-demo/conf/galley.demo.yaml +++ b/deploy/services-demo/conf/galley.demo.yaml @@ -28,6 +28,9 @@ settings: conversationCodeURI: https://127.0.0.1/conversation-join/ concurrentDeletionEvents: 1024 deleteConvThrottleMillis: 0 + mlsPrivateKeyPaths: + removal: + ed25519: conf/ed25519.pem featureFlags: # see #RefConfigOptions in `/docs/reference` sso: disabled-by-default diff --git a/docs/legacy/developer/api-versioning.md b/docs/legacy/developer/api-versioning.md index 1bae9bfe46a..f39fb566da1 100644 --- a/docs/legacy/developer/api-versioning.md +++ b/docs/legacy/developer/api-versioning.md @@ -120,8 +120,9 @@ version. #### Adding a new endpoint -We add the new endpoint to the routing table, and set its version range to only -include the development version. The supported version is unaffected. +We add the new endpoint to the routing table. There is no need to set its +version range to only include the development version, since the supported +version is unaffected. #### Removing an endpoint diff --git a/docs/legacy/reference/config-options.md b/docs/legacy/reference/config-options.md index 33d8eb04445..f40eadba37a 100644 --- a/docs/legacy/reference/config-options.md +++ b/docs/legacy/reference/config-options.md @@ -26,6 +26,30 @@ Even when the flag is `disabled`, galley will keep writing to the been added in order to deploy new code and backfill data in production. +### MLS private key paths + +The `mlsPrivateKeyPaths` field should contain a mapping from *purposes* and +signature schemes to file paths of corresponding x509 private keys in PEM +format. + +At the moment, the only purpose is `removal`, meaning that the key will be used +to sign external remove proposals. + +For example: + +``` + mlsPrivateKeyPaths: + removal: + ed25519: /etc/secrets/ed25519.pem +``` + +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 / +``` + ## 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 b1c5191414d..02d212832d4 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -167,6 +167,13 @@ galley: secrets: awsKeyId: dummykey awsSecretKey: dummysecret + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c + -----END PRIVATE KEY----- + gundeck: replicaCount: 1 imagePullPolicy: {{ .Values.imagePullPolicy }} diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index 3b31e199d60..2dd9f9013f3 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -84,10 +84,11 @@ csHash MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 ctx value = HKDF.expand (HKDF.extract @SHA256 (mempty :: ByteString) value) ctx 16 csVerifySignature :: CipherSuiteTag -> ByteString -> ByteString -> ByteString -> Bool -csVerifySignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pub x sig = fromMaybe False . maybeCryptoError $ do - pub' <- Ed25519.publicKey pub - sig' <- Ed25519.signature sig - pure $ Ed25519.verify pub' x sig' +csVerifySignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pub x sig = + fromMaybe False . maybeCryptoError $ do + pub' <- Ed25519.publicKey pub + sig' <- Ed25519.signature sig + pure $ Ed25519.verify pub' x sig' csSignatureScheme :: CipherSuiteTag -> SignatureSchemeTag csSignatureScheme MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = Ed25519 diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index b2af79881aa..7077569f3d5 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -24,6 +24,7 @@ import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson +import Data.Bifunctor import Data.Binary import Data.Binary.Get import Data.Binary.Parser @@ -156,3 +157,41 @@ instance ParseMLS 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.SwaggerString + +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/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs new file mode 100644 index 00000000000..df7989ffd95 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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.Keys + ( MLSKeys (..), + MLSPublicKeys (..), + mlsKeysToPublic, + ) +where + +import Crypto.PubKey.Ed25519 +import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.ByteArray +import Data.Json.Util +import qualified Data.Map as Map +import Data.Schema +import qualified Data.Swagger as S +import Imports +import Wire.API.MLS.Credential + +data MLSKeys = MLSKeys + { mlsKeyPair_ed25519 :: Maybe (SecretKey, PublicKey) + } + +instance Semigroup MLSKeys where + MLSKeys Nothing <> MLSKeys ed2 = MLSKeys ed2 + MLSKeys ed1 <> MLSKeys _ = MLSKeys ed1 + +instance Monoid MLSKeys where + mempty = MLSKeys Nothing + +newtype MLSPublicKeys = MLSPublicKeys + { unMLSPublicKeys :: Map SignaturePurpose (Map SignatureSchemeTag ByteString) + } + deriving (FromJSON, ToJSON, S.ToSchema) via Schema MLSPublicKeys + deriving newtype (Semigroup, Monoid) + +instance ToSchema MLSPublicKeys where + schema = + named "MLSKeys" $ + MLSPublicKeys <$> unMLSPublicKeys + .= map_ (map_ base64Schema) + +mlsKeysToPublic1 :: MLSKeys -> Map SignatureSchemeTag ByteString +mlsKeysToPublic1 (MLSKeys mEd25519key) = + fold $ Map.singleton Ed25519 . convert . snd <$> mEd25519key + +mlsKeysToPublic :: (SignaturePurpose -> MLSKeys) -> MLSPublicKeys +mlsKeysToPublic f = flip foldMap [minBound .. maxBound] $ \purpose -> + MLSPublicKeys (Map.singleton purpose (mlsKeysToPublic1 (f purpose))) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 45d6886809c..323aebaded3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -41,6 +41,7 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as BrigError import Wire.API.Error.Galley import Wire.API.Event.Conversation +import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.Servant @@ -1363,6 +1364,12 @@ type MLSMessagingAPI = :> ReqBody '[MLS] (RawMLS SomeMessage) :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) ) + :<|> Named + "mls-public-keys" + ( Summary "Get public keys used by the backend to sign external proposals" + :> "public-keys" + :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" MLSPublicKeys) + ) type MLSAPI = LiftNamed (ZLocalUser :> "mls" :> MLSMessagingAPI) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index d99d652103c..ae55a5fc535 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -48,6 +48,7 @@ library Wire.API.MLS.Extension Wire.API.MLS.Group Wire.API.MLS.KeyPackage + Wire.API.MLS.Keys Wire.API.MLS.Message Wire.API.MLS.Proposal Wire.API.MLS.Serialisation diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 948dd33b483..00b65e9cece 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -32,6 +32,7 @@ library Galley.API.Message Galley.API.MLS Galley.API.MLS.KeyPackage + Galley.API.MLS.Keys Galley.API.MLS.Message Galley.API.MLS.Welcome Galley.API.One2One @@ -115,6 +116,7 @@ library Galley.Intra.Team Galley.Intra.User Galley.Intra.Util + Galley.Keys Galley.Monad Galley.Options Galley.Queue @@ -175,6 +177,8 @@ library aeson >=2.0.1.0 , amazonka >=1.4.5 , amazonka-sqs >=1.4.5 + , asn1-encoding + , asn1-types , async >=2.0 , base >=4.6 && <5 , base64-bytestring >=1.0 @@ -270,6 +274,7 @@ library , warp >=3.0 , wire-api , wire-api-federation + , x509 default-language: Haskell2010 diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index c5068b63784..be7d1fe7d62 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -42,6 +42,9 @@ settings: # Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working # Remember to keep it the same in Brig federationDomain: example.com + mlsPrivateKeyPaths: + removal: + ed25519: test/resources/ed25519.pem featureFlags: # see #RefConfigOptions in `/docs/reference` sso: disabled-by-default diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index 1e89f9bd590..fc85496141b 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -20,8 +20,10 @@ module Galley.API.MLS postMLSMessage, postMLSMessageFromLocalUser, postMLSMessageFromLocalUserV1, + getMLSPublicKeys, ) where +import Galley.API.MLS.Keys import Galley.API.MLS.Message import Galley.API.MLS.Welcome diff --git a/services/galley/src/Galley/API/MLS/Keys.hs b/services/galley/src/Galley/API/MLS/Keys.hs new file mode 100644 index 00000000000..43890eb9ed3 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Keys.hs @@ -0,0 +1,35 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Keys where + +import Control.Lens (view) +import Data.Id +import Data.Qualified +import Galley.Env +import Imports +import Polysemy +import Polysemy.Input +import Wire.API.MLS.Keys + +getMLSPublicKeys :: + Member (Input Env) r => + Local UserId -> + Sem r MLSPublicKeys +getMLSPublicKeys _ = do + keys <- inputs (view mlsKeys) + pure $ mlsKeysToPublic keys diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 9ee3de4c20c..21378d52784 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -161,6 +161,7 @@ servantSitemap = mkNamedAPI @"mls-welcome-message" postMLSWelcome <@> mkNamedAPI @"mls-message-v1" postMLSMessageFromLocalUserV1 <@> mkNamedAPI @"mls-message" postMLSMessageFromLocalUser + <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys customBackend :: API CustomBackendAPI GalleyEffects customBackend = mkNamedAPI @"get-custom-backend-by-domain" getCustomBackendByDomain diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 7ea627cc3b4..a145b9b446d 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -80,6 +80,7 @@ import Galley.Env import Galley.External import Galley.Intra.Effects import Galley.Intra.Federator +import Galley.Keys import Galley.Options import Galley.Queue import qualified Galley.Queue as Q @@ -157,6 +158,7 @@ createEnv m o = do <$> Q.new 16000 <*> initExtEnv <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. optJournal) + <*> loadAllMLSKeys (fold (o ^. optSettings . setMlsPrivateKeyPaths)) initCassandra :: Opts -> Logger -> IO ClientState initCassandra o l = do diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 8958e3cdc73..76eecaa2cf3 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -38,6 +38,8 @@ import qualified OpenSSL.X509.SystemStore 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 data DeleteItem = TeamItem TeamId UserId (Maybe ConnId) @@ -55,7 +57,8 @@ data Env = Env _cstate :: ClientState, _deleteQueue :: Q.Queue DeleteItem, _extEnv :: ExtEnv, - _aEnv :: Maybe Aws.Env + _aEnv :: Maybe Aws.Env, + _mlsKeys :: SignaturePurpose -> MLSKeys } -- | Environment specific to the communication with external diff --git a/services/galley/src/Galley/Keys.hs b/services/galley/src/Galley/Keys.hs new file mode 100644 index 00000000000..129b42396a3 --- /dev/null +++ b/services/galley/src/Galley/Keys.hs @@ -0,0 +1,90 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 . + +-- | Handling of MLS private keys used for signing external proposals. +module Galley.Keys + ( MLSPrivateKeyPaths, + loadAllMLSKeys, + ) +where + +import Control.Exception +import Crypto.PubKey.Ed25519 +import Data.ASN1.BinaryEncoding +import Data.ASN1.Encoding +import Data.ASN1.Types +import Data.Bifunctor +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Map as Map +import Data.PEM +import Data.X509 +import Imports +import Wire.API.MLS.Credential +import Wire.API.MLS.Keys + +type MLSPrivateKeyPaths = Map SignaturePurpose (Map SignatureSchemeTag FilePath) + +data MLSPrivateKeyException = MLSPrivateKeyException + { mpkePath :: FilePath, + mpkeMsg :: String + } + deriving (Eq, Show, Typeable) + +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 (SignaturePurpose -> MLSKeys) +loadAllMLSKeys = fmap mapToFunction . traverse loadMLSKeys + +loadMLSKeys :: Map SignatureSchemeTag FilePath -> IO MLSKeys +loadMLSKeys m = + MLSKeys + <$> traverse loadEd25519KeyPair (Map.lookup Ed25519 m) + +loadEd25519KeyPair :: FilePath -> IO (SecretKey, PublicKey) +loadEd25519KeyPair path = do + bytes <- LBS.readFile path + priv <- + either (throwIO . MLSPrivateKeyException path) pure $ + decodeEd25519PrivateKey bytes + pure (priv, toPublic priv) + +decodeEd25519PrivateKey :: + LByteString -> + Either String SecretKey +decodeEd25519PrivateKey bytes = do + pems <- pemParseLBS bytes + pem <- expectOne "private key" pems + let content = pemContent pem + asn1 <- first displayException (decodeASN1' BER content) + (priv, remainder) <- fromASN1 asn1 + expectEmpty remainder + case priv of + 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" diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index d8782870b6b..2d822419134 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -29,6 +29,7 @@ module Galley.Options setDeleteConvThrottleMillis, setFederationDomain, setEnableIndexedBillingTeamMembers, + setMlsPrivateKeyPaths, setFeatureFlags, defConcurrentDeletionEvents, defDeleteConvThrottleMillis, @@ -57,6 +58,7 @@ import Data.Aeson.TH (deriveFromJSON) import Data.Domain (Domain) import Data.Misc import Data.Range +import Galley.Keys import Galley.Types.Teams import Imports import System.Logger.Extended (Level, LogFormat) @@ -103,6 +105,7 @@ data Settings = Settings -- the owners. -- Defaults to false. _setEnableIndexedBillingTeamMembers :: !(Maybe Bool), + _setMlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _setFeatureFlags :: !FeatureFlags } diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 7e617a9d8df..79dc58f7cb3 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -37,6 +37,7 @@ import Data.Json.Util hiding ((#)) import qualified Data.List.NonEmpty as NE import qualified Data.List.NonEmpty as NonEmpty import Data.List1 hiding (head) +import qualified Data.Map as Map import Data.Qualified import Data.Range import qualified Data.Set as Set @@ -64,7 +65,9 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Credential import Wire.API.MLS.Group (convToGroupId) +import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.Message import Wire.API.Routes.Version @@ -148,7 +151,8 @@ tests s = test s "add users bypassing MLS" testAddUsersDirectly, test s "remove users bypassing MLS" testRemoveUsersDirectly, test s "send proteus message to an MLS conversation" testProteusMessage - ] + ], + test s "public keys" testPublicKeys ] postMLSConvFail :: TestM () @@ -1396,3 +1400,24 @@ testExternalAddProposalWrongClient = withSystemTempDirectory "mls" $ \tmp -> do const (Just "mls-unsupported-proposal") === 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 + ) +