diff --git a/changelog.d/8772.misc b/changelog.d/8772.misc
new file mode 100644
index 0000000000..d74d0a3d5d
--- /dev/null
+++ b/changelog.d/8772.misc
@@ -0,0 +1 @@
+Add a commandline script to sign arbitrary json objects.
diff --git a/mypy.ini b/mypy.ini
index 0cf7c93f45..f4f981e813 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -8,6 +8,7 @@ show_traceback = True
mypy_path = stubs
warn_unreachable = True
files =
+ scripts-dev/sign_json,
synapse/api,
synapse/appservice,
synapse/config,
diff --git a/scripts-dev/sign_json b/scripts-dev/sign_json
new file mode 100755
index 0000000000..44553fb79a
--- /dev/null
+++ b/scripts-dev/sign_json
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+#
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import argparse
+import json
+import sys
+from json import JSONDecodeError
+
+import yaml
+from signedjson.key import read_signing_keys
+from signedjson.sign import sign_json
+
+from synapse.util import json_encoder
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="""Adds a signature to a JSON object.
+
+Example usage:
+
+ $ scripts-dev/sign_json.py -N test -k localhost.signing.key "{}"
+ {"signatures":{"test":{"ed25519:a_ZnZh":"LmPnml6iM0iR..."}}}
+""",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ parser.add_argument(
+ "-N",
+ "--server-name",
+ help="Name to give as the local homeserver. If unspecified, will be "
+ "read from the config file.",
+ )
+
+ parser.add_argument(
+ "-k",
+ "--signing-key-path",
+ help="Path to the file containing the private ed25519 key to sign the "
+ "request with.",
+ )
+
+ parser.add_argument(
+ "-c",
+ "--config",
+ default="homeserver.yaml",
+ help=(
+ "Path to synapse config file, from which the server name and/or signing "
+ "key path will be read. Ignored if --server-name and --signing-key-path "
+ "are both given."
+ ),
+ )
+
+ input_args = parser.add_mutually_exclusive_group()
+
+ input_args.add_argument("input_data", nargs="?", help="Raw JSON to be signed.")
+
+ input_args.add_argument(
+ "-i",
+ "--input",
+ type=argparse.FileType("r"),
+ default=sys.stdin,
+ help=(
+ "A file from which to read the JSON to be signed. If neither --input nor "
+ "input_data are given, JSON will be read from stdin."
+ ),
+ )
+
+ parser.add_argument(
+ "-o",
+ "--output",
+ type=argparse.FileType("w"),
+ default=sys.stdout,
+ help="Where to write the signed JSON. Defaults to stdout.",
+ )
+
+ args = parser.parse_args()
+
+ if not args.server_name or not args.signing_key_path:
+ read_args_from_config(args)
+
+ with open(args.signing_key_path) as f:
+ key = read_signing_keys(f)[0]
+
+ json_to_sign = args.input_data
+ if json_to_sign is None:
+ json_to_sign = args.input.read()
+
+ try:
+ obj = json.loads(json_to_sign)
+ except JSONDecodeError as e:
+ print("Unable to parse input as JSON: %s" % e, file=sys.stderr)
+ sys.exit(1)
+
+ if not isinstance(obj, dict):
+ print("Input json was not an object", file=sys.stderr)
+ sys.exit(1)
+
+ sign_json(obj, args.server_name, key)
+ for c in json_encoder.iterencode(obj):
+ args.output.write(c)
+ args.output.write("\n")
+
+
+def read_args_from_config(args: argparse.Namespace) -> None:
+ with open(args.config, "r") as fh:
+ config = yaml.safe_load(fh)
+ if not args.server_name:
+ args.server_name = config["server_name"]
+ if not args.signing_key_path:
+ args.signing_key_path = config["signing_key_path"]
+
+
+if __name__ == "__main__":
+ main()
|