Skip to content

Commit

Permalink
Support Synapse's (and technically Dendrite's) multi-key format
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Nov 26, 2023
1 parent 5a676a6 commit 50de519
Show file tree
Hide file tree
Showing 13 changed files with 642 additions and 129 deletions.
44 changes: 44 additions & 0 deletions cmd/utilities/_common/signing_key_export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package _common

import (
"os"

"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/homeserver_interop"
"github.com/turt2live/matrix-media-repo/homeserver_interop/dendrite"
"github.com/turt2live/matrix-media-repo/homeserver_interop/mmr"
"github.com/turt2live/matrix-media-repo/homeserver_interop/synapse"
)

func EncodeSigningKeys(keys []*homeserver_interop.SigningKey, format string, file string) {
var err error
var b []byte
switch format {
case "synapse":
b, err = synapse.EncodeAllSigningKeys(keys)
case "dendrite":
b, err = dendrite.EncodeAllSigningKeys(keys)
case "mmr":
b, err = mmr.EncodeAllSigningKeys(keys)
default:
logrus.Fatalf("Unknown output format '%s'. Try '%s -help' for information.", format, os.Args[0])
}
if err != nil {
logrus.Fatal(err)
}

f, err := os.Create(file)
if err != nil {
logrus.Fatal(err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)

_, err = f.Write(b)
if err != nil {
logrus.Fatal(err)
}

logrus.Infof("Done! Signing key written to '%s' in %s format", f.Name(), format)
}
50 changes: 50 additions & 0 deletions cmd/utilities/combine_signing_keys/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"flag"
"os"

"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/cmd/utilities/_common"
"github.com/turt2live/matrix-media-repo/homeserver_interop"
"github.com/turt2live/matrix-media-repo/homeserver_interop/any_server"
"github.com/turt2live/matrix-media-repo/util"
)

func main() {
outputFormat := flag.String("format", "mmr", "The output format for the key. May be 'mmr', 'synapse', or 'dendrite'.")
outputFile := flag.String("output", "./signing.key", "The output file for the key. Note that not all software will use multiple keys.")
flag.Parse()

keys := make(map[string]*homeserver_interop.SigningKey)
keysArray := make([]*homeserver_interop.SigningKey, 0)
for _, file := range flag.Args() {
logrus.Infof("Reading %s", file)

localKeys, err := decodeKeys(file)
if err != nil {
logrus.Fatal(err)
}

for _, key := range localKeys {
if val, ok := keys[key.KeyVersion]; ok {
logrus.Fatalf("Duplicate key version '%s' detected. Known='%s', duplicate='%s'", key.KeyVersion, util.EncodeUnpaddedBase64ToString(val.PrivateKey), util.EncodeUnpaddedBase64ToString(key.PrivateKey))
}

keys[key.KeyVersion] = key
keysArray = append(keysArray, key)
}
}

_common.EncodeSigningKeys(keysArray, *outputFormat, *outputFile)
}

func decodeKeys(fileName string) ([]*homeserver_interop.SigningKey, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer f.Close()

return any_server.DecodeAllSigningKeys(f)
}
55 changes: 16 additions & 39 deletions cmd/utilities/generate_signing_key/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,41 @@ import (
"strings"

"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/cmd/utilities/_common"
"github.com/turt2live/matrix-media-repo/homeserver_interop"
"github.com/turt2live/matrix-media-repo/homeserver_interop/any_server"
"github.com/turt2live/matrix-media-repo/homeserver_interop/dendrite"
"github.com/turt2live/matrix-media-repo/homeserver_interop/mmr"
"github.com/turt2live/matrix-media-repo/homeserver_interop/synapse"
)

func main() {
inputFile := flag.String("input", "", "When set to a file path, the signing key to convert to the output format. The key must have been generated in a format supported by -format.")
inputFile := flag.String("input", "", "When set to a file path, the signing key to convert to the output format. The key must have been generated in a format supported by -format. If the format supports multiple keys, only the first will be converted.")
outputFormat := flag.String("format", "mmr", "The output format for the key. May be 'mmr', 'synapse', or 'dendrite'.")
outputFile := flag.String("output", "./signing.key", "The output file for the key.")
flag.Parse()

var keyVersion string
var priv ed25519.PrivateKey
var key *homeserver_interop.SigningKey
var err error

if *inputFile != "" {
priv, keyVersion, err = decodeKey(*inputFile)
key, err = decodeKey(*inputFile)
} else {
keyVersion = makeKeyVersion()
keyVersion := makeKeyVersion()

var priv ed25519.PrivateKey
_, priv, err = ed25519.GenerateKey(nil)
priv = priv[len(priv)-32:]
}
if err != nil {
logrus.Fatal(err)
}

logrus.Infof("Key ID will be 'ed25519:%s'", keyVersion)

var b []byte
switch *outputFormat {
case "synapse":
b, err = synapse.EncodeSigningKey(keyVersion, priv)
case "dendrite":
b, err = dendrite.EncodeSigningKey(keyVersion, priv)
case "mmr":
b, err = mmr.EncodeSigningKey(keyVersion, priv)
default:
logrus.Fatalf("Unknown output format '%s'. Try '%s -help' for information.", *outputFormat, flag.Arg(0))
key = &homeserver_interop.SigningKey{
PrivateKey: priv,
KeyVersion: keyVersion,
}
}
if err != nil {
logrus.Fatal(err)
}

f, err := os.Create(*outputFile)
if err != nil {
logrus.Fatal(err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)

_, err = f.Write(b)
if err != nil {
logrus.Fatal(err)
}
logrus.Infof("Key ID will be 'ed25519:%s'", key.KeyVersion)

logrus.Infof("Done! Signing key written to '%s' in %s format", f.Name(), *outputFormat)
_common.EncodeSigningKeys([]*homeserver_interop.SigningKey{key}, *outputFormat, *outputFile)
}

func makeKeyVersion() string {
Expand All @@ -92,10 +69,10 @@ func makeKeyVersion() string {
return strings.Join(chars[:6], "")
}

func decodeKey(fileName string) (ed25519.PrivateKey, string, error) {
func decodeKey(fileName string) (*homeserver_interop.SigningKey, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, "", err
return nil, err
}
defer f.Close()

Expand Down
34 changes: 21 additions & 13 deletions homeserver_interop/any_server/signing_key.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
package any_server

import (
"crypto/ed25519"
"errors"
"io"

"github.com/turt2live/matrix-media-repo/homeserver_interop"
"github.com/turt2live/matrix-media-repo/homeserver_interop/dendrite"
"github.com/turt2live/matrix-media-repo/homeserver_interop/mmr"
"github.com/turt2live/matrix-media-repo/homeserver_interop/synapse"
)

func DecodeSigningKey(key io.ReadSeeker) (ed25519.PrivateKey, string, error) {
var keyVersion string
var priv ed25519.PrivateKey
func DecodeSigningKey(key io.ReadSeeker) (*homeserver_interop.SigningKey, error) {
keys, err := DecodeAllSigningKeys(key)
if err != nil {
return nil, err
}

return keys[0], nil
}

func DecodeAllSigningKeys(key io.ReadSeeker) ([]*homeserver_interop.SigningKey, error) {
var keys []*homeserver_interop.SigningKey
var err error

var errorStack error

// Try Synapse first, as the most popular
priv, keyVersion, err = synapse.DecodeSigningKey(key)
keys, err = synapse.DecodeAllSigningKeys(key)
if err == nil {
return priv, keyVersion, nil
return keys, nil
}
errorStack = errors.Join(errors.New("synapse: unable to decode"), err, errorStack)

// Rewind & try Dendrite
if _, err = key.Seek(0, io.SeekStart); err != nil {
return nil, "", err
return nil, err
}
priv, keyVersion, err = dendrite.DecodeSigningKey(key)
keys, err = dendrite.DecodeAllSigningKeys(key)
if err == nil {
return priv, keyVersion, nil
return keys, nil
}
errorStack = errors.Join(errors.New("dendrite: unable to decode"), err, errorStack)

// Rewind & try MMR
if _, err = key.Seek(0, io.SeekStart); err != nil {
return nil, "", err
return nil, err
}
priv, keyVersion, err = mmr.DecodeSigningKey(key)
keys, err = mmr.DecodeAllSigningKeys(key)
if err == nil {
return priv, keyVersion, nil
return keys, nil
}
errorStack = errors.Join(errors.New("mmr: unable to decode"), err, errorStack)

// Fail case
return nil, "", errors.Join(errors.New("unable to detect signing key format"), errorStack)
return nil, errors.Join(errors.New("unable to detect signing key format"), errorStack)
}
48 changes: 36 additions & 12 deletions homeserver_interop/dendrite/signing_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,75 @@ import (
"fmt"
"io"
"strings"

"github.com/turt2live/matrix-media-repo/homeserver_interop"
"github.com/turt2live/matrix-media-repo/homeserver_interop/internal"
)

const blockType = "MATRIX PRIVATE KEY"

func EncodeSigningKey(keyVersion string, key ed25519.PrivateKey) ([]byte, error) {
func EncodeSigningKey(key *homeserver_interop.SigningKey) ([]byte, error) {
block := &pem.Block{
Type: blockType,
Headers: map[string]string{
"Key-ID": fmt.Sprintf("ed25519:%s", keyVersion),
"Key-ID": fmt.Sprintf("ed25519:%s", key.KeyVersion),
},
Bytes: key.Seed(),
Bytes: key.PrivateKey.Seed(),
}
return pem.EncodeToMemory(block), nil
}

func DecodeSigningKey(key io.Reader) (ed25519.PrivateKey, string, error) {
func EncodeAllSigningKeys(keys []*homeserver_interop.SigningKey) ([]byte, error) {
return internal.EncodeNewlineAppendFormattedSigningKeys(keys, EncodeSigningKey)
}

func DecodeSigningKey(key io.Reader) (*homeserver_interop.SigningKey, error) {
keys, err := DecodeAllSigningKeys(key)
if err != nil {
return nil, err
}

return keys[0], nil
}

func DecodeAllSigningKeys(key io.Reader) ([]*homeserver_interop.SigningKey, error) {
b, err := io.ReadAll(key)
if err != nil {
return nil, "", err
return nil, err
}

keys := make([]*homeserver_interop.SigningKey, 0)
var block *pem.Block
for {
block, b = pem.Decode(b)
if b == nil {
return nil, "", fmt.Errorf("no signing key found")
return nil, fmt.Errorf("no signing key found")
}
if block == nil {
return nil, "", fmt.Errorf("unable to read suitable block from PEM file")
if block == nil && (len(keys) == 0 || len(b) > 0) {
return nil, fmt.Errorf("unable to read suitable block from PEM file")
} else if block == nil && len(b) == 0 {
break
}
if block.Type == blockType {
keyId := block.Headers["Key-ID"]
if len(keyId) <= 0 {
return nil, "", fmt.Errorf("missing Key-ID header")
return nil, fmt.Errorf("missing Key-ID header")
}
if !strings.HasPrefix(keyId, "ed25519:") {
return nil, "", fmt.Errorf("key ID '%s' does not denote an ed25519 private key", keyId)
return nil, fmt.Errorf("key ID '%s' does not denote an ed25519 private key", keyId)
}

_, priv, err := ed25519.GenerateKey(bytes.NewReader(block.Bytes))
if err != nil {
return nil, "", err
return nil, err
}

return priv, keyId[len("ed25519:"):], nil
keys = append(keys, &homeserver_interop.SigningKey{
PrivateKey: priv,
KeyVersion: keyId[len("ed25519:"):],
})
}
}

return keys, nil
}
37 changes: 37 additions & 0 deletions homeserver_interop/internal/signing_key_encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package internal

import (
"bytes"
"fmt"

"github.com/turt2live/matrix-media-repo/homeserver_interop"
)

func EncodeNewlineAppendFormattedSigningKeys(keys []*homeserver_interop.SigningKey, encodeFn func(*homeserver_interop.SigningKey) ([]byte, error)) ([]byte, error) {
buf := &bytes.Buffer{}
for i, key := range keys {
b, err := encodeFn(key)
if err != nil {
return nil, err
}

n, err := buf.Write(b)
if err != nil {
return nil, err
}
if n != len(b) {
return nil, fmt.Errorf("wrote %d bytes but expected %d bytes", n, len(b))
}

if b[len(b)-1] != '\n' && i != (len(keys)-1) {
n, err = buf.Write([]byte{'\n'})
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("wrote %d bytes but expected %d bytes", n, 1)
}
}
}
return buf.Bytes(), nil
}
Loading

0 comments on commit 50de519

Please sign in to comment.