diff --git a/changelog.d/11398.feature b/changelog.d/11398.feature
new file mode 100644
index 0000000000..a910f4da14
--- /dev/null
+++ b/changelog.d/11398.feature
@@ -0,0 +1 @@
+Implement [MSC3383](https://github.com/matrix-org/matrix-spec-proposals/pull/3383) for including the destination in server-to-server authentication headers. Contributed by @Bubu and @jcgruenhage for Famedly GmbH.
diff --git a/scripts-dev/federation_client.py b/scripts-dev/federation_client.py
index c72e19f61d..079d2f5ed0 100755
--- a/scripts-dev/federation_client.py
+++ b/scripts-dev/federation_client.py
@@ -124,7 +124,12 @@ def request(
authorization_headers = []
for key, sig in signed_json["signatures"][origin_name].items():
- header = 'X-Matrix origin=%s,key="%s",sig="%s"' % (origin_name, key, sig)
+ header = 'X-Matrix origin=%s,key="%s",sig="%s",destination="%s"' % (
+ origin_name,
+ key,
+ sig,
+ destination,
+ )
authorization_headers.append(header.encode("ascii"))
print("Authorization: %s" % header, file=sys.stderr)
diff --git a/synapse/federation/transport/server/_base.py b/synapse/federation/transport/server/_base.py
index 2529dee613..d629a3ecb5 100644
--- a/synapse/federation/transport/server/_base.py
+++ b/synapse/federation/transport/server/_base.py
@@ -16,7 +16,8 @@ import functools
import logging
import re
import time
-from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Tuple, cast
+from http import HTTPStatus
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Tuple, cast
from synapse.api.errors import Codes, FederationDeniedError, SynapseError
from synapse.api.urls import FEDERATION_V1_PREFIX
@@ -86,15 +87,24 @@ class Authenticator:
if not auth_headers:
raise NoAuthenticationError(
- 401, "Missing Authorization headers", Codes.UNAUTHORIZED
+ HTTPStatus.UNAUTHORIZED,
+ "Missing Authorization headers",
+ Codes.UNAUTHORIZED,
)
for auth in auth_headers:
if auth.startswith(b"X-Matrix"):
- (origin, key, sig) = _parse_auth_header(auth)
+ (origin, key, sig, destination) = _parse_auth_header(auth)
json_request["origin"] = origin
json_request["signatures"].setdefault(origin, {})[key] = sig
+ # if the origin_server sent a destination along it needs to match our own server_name
+ if destination is not None and destination != self.server_name:
+ raise AuthenticationError(
+ HTTPStatus.UNAUTHORIZED,
+ "Destination mismatch in auth header",
+ Codes.UNAUTHORIZED,
+ )
if (
self.federation_domain_whitelist is not None
and origin not in self.federation_domain_whitelist
@@ -103,7 +113,9 @@ class Authenticator:
if origin is None or not json_request["signatures"]:
raise NoAuthenticationError(
- 401, "Missing Authorization headers", Codes.UNAUTHORIZED
+ HTTPStatus.UNAUTHORIZED,
+ "Missing Authorization headers",
+ Codes.UNAUTHORIZED,
)
await self.keyring.verify_json_for_server(
@@ -142,13 +154,14 @@ class Authenticator:
logger.exception("Error resetting retry timings on %s", origin)
-def _parse_auth_header(header_bytes: bytes) -> Tuple[str, str, str]:
+def _parse_auth_header(header_bytes: bytes) -> Tuple[str, str, str, Optional[str]]:
"""Parse an X-Matrix auth header
Args:
header_bytes: header value
Returns:
+ origin, key id, signature, destination.
origin, key id, signature.
Raises:
@@ -157,7 +170,9 @@ def _parse_auth_header(header_bytes: bytes) -> Tuple[str, str, str]:
try:
header_str = header_bytes.decode("utf-8")
params = header_str.split(" ")[1].split(",")
- param_dict = {k: v for k, v in (kv.split("=", maxsplit=1) for kv in params)}
+ param_dict: Dict[str, str] = {
+ k: v for k, v in [param.split("=", maxsplit=1) for param in params]
+ }
def strip_quotes(value: str) -> str:
if value.startswith('"'):
@@ -172,7 +187,15 @@ def _parse_auth_header(header_bytes: bytes) -> Tuple[str, str, str]:
key = strip_quotes(param_dict["key"])
sig = strip_quotes(param_dict["sig"])
- return origin, key, sig
+
+ # get the destination server_name from the auth header if it exists
+ destination = param_dict.get("destination")
+ if destination is not None:
+ destination = strip_quotes(destination)
+ else:
+ destination = None
+
+ return origin, key, sig, destination
except Exception as e:
logger.warning(
"Error parsing auth header '%s': %s",
@@ -180,7 +203,7 @@ def _parse_auth_header(header_bytes: bytes) -> Tuple[str, str, str]:
e,
)
raise AuthenticationError(
- 400, "Malformed Authorization header", Codes.UNAUTHORIZED
+ HTTPStatus.BAD_REQUEST, "Malformed Authorization header", Codes.UNAUTHORIZED
)
diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py
index 5097b3ca57..e686445955 100644
--- a/synapse/http/matrixfederationclient.py
+++ b/synapse/http/matrixfederationclient.py
@@ -704,6 +704,9 @@ class MatrixFederationHttpClient:
Returns:
A list of headers to be added as "Authorization:" headers
"""
+ if destination is None and destination_is is None:
+ raise ValueError("destination and destination_is cannot both be None!")
+
request: JsonDict = {
"method": method.decode("ascii"),
"uri": url_bytes.decode("ascii"),
@@ -726,8 +729,13 @@ class MatrixFederationHttpClient:
for key, sig in request["signatures"][self.server_name].items():
auth_headers.append(
(
- 'X-Matrix origin=%s,key="%s",sig="%s"'
- % (self.server_name, key, sig)
+ 'X-Matrix origin=%s,key="%s",sig="%s",destination="%s"'
+ % (
+ self.server_name,
+ key,
+ sig,
+ request.get("destination") or request["destination_is"],
+ )
).encode("ascii")
)
return auth_headers
|