Skip to content

Commit

Permalink
New MLS ciphersuites (#3964)
Browse files Browse the repository at this point in the history
* Add one ECDSA ciphersuite

* Fix ECDSA signature decoding

* Create test clients using correct signature scheme

* Fix unsupported ciphersuite test

* Create one mls-test-cli store per signature scheme

* Add MLS_256_DHKEMP384_AES256GCM_SHA384_P384

* Add MLS_256_DHKEMP521_AES256GCM_SHA512_P521

* Fix secp384 signature verification

* Fix x509 credential validation

* Update mls-test-cli to 0.11

* Turn TODO into FUTUREWORK

* Add failing test showing incorrect backend signature

* Store private keys for other signature schemes

* Parse ECDSA private keys

* Encode ECDSA signatures

* Pass removal key correctly to mls-test-cli

* MLSKeys: Move from maps to records for config and public key endpoint

* Adapt to MLSKeys changes in galley

* Move GET /mls/public-keys test to new integration suite

* Remove SignaturePurpose type

* Add golden tests for MLSKeys

The JSON files were generated using the code before this refactoring

* Document new removal key config options

* Test public key endpoint when MLS is not enabled

* Fix galley configmap

* Make withCiphersuite exception-safe

---------

Co-authored-by: Akshay Mankar <[email protected]>
  • Loading branch information
pcapriotti and akshaymankar authored Apr 24, 2024
1 parent cf1e857 commit f57321b
Show file tree
Hide file tree
Showing 61 changed files with 898 additions and 313 deletions.
1 change: 1 addition & 0 deletions changelog.d/0-release-notes/new-ciphersuites
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions charts/galley/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
9 changes: 9 additions & 0 deletions charts/galley/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
13 changes: 12 additions & 1 deletion docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions hack/helm_vars/wire-server/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,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 }}
Expand Down
5 changes: 5 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
93 changes: 51 additions & 42 deletions integration/test/MLS/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ mlscli :: (HasCallStack) => ClientIdentity -> [String] -> Maybe ByteString -> Ap
mlscli cid args mbstdin = do
groupOut <- randomFileName
let substOut = argSubst "<group-out>" groupOut
cs <- (.ciphersuite) <$> getMLSState
let scheme = csSignatureScheme cs

gs <- getClientGroupState cid

Expand All @@ -87,23 +89,24 @@ mlscli cid args mbstdin = do
Just groupData -> do
fn <- toRandomFile groupData
pure (argSubst "<group-in>" 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` ["<group-in>", "<group-out>"]) $
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
Expand All @@ -113,12 +116,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
Expand All @@ -129,46 +143,27 @@ createWireClient u = do
>>= getJSON 201
>>= mkClientIdentity u

data CredentialType = BasicCredentialType | X509CredentialType

instance MakesValue CredentialType where
make BasicCredentialType = make "basic"
make X509CredentialType = make "x509"

instance TestCases CredentialType where
testCases =
[ MkTestCase "[ctype=basic]" BasicCredentialType,
MkTestCase "[ctype=x509]" X509CredentialType
]

data InitMLSClient = InitMLSClient
{credType :: CredentialType}

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
Expand All @@ -177,17 +172,17 @@ 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 ->
resp.status `shouldMatchInt` 201

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
Expand Down Expand Up @@ -244,8 +239,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
Expand Down Expand Up @@ -706,7 +704,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 =
Expand Down Expand Up @@ -761,6 +759,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 ->
Expand All @@ -778,7 +787,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
Expand Down
Loading

0 comments on commit f57321b

Please sign in to comment.