Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add signing key generation and conversion tooling #512

Merged
merged 5 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.key
/webui
/.idea
/bin
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.key
/webui
/.idea
/bin
Expand Down
103 changes: 103 additions & 0 deletions cmd/utilities/generate_signing_key/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"crypto/ed25519"
"crypto/rand"
"flag"
"fmt"
"os"
"sort"
"strings"

"github.com/sirupsen/logrus"
"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.")
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 err error

if *inputFile != "" {
priv, keyVersion, err = decodeKey(*inputFile)
} else {
keyVersion = makeKeyVersion()
_, 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))
}
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("Done! Signing key written to '%s' in %s format", f.Name(), *outputFormat)
}

func makeKeyVersion() string {
buf := make([]byte, 2)
chars := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", "")
for i := 0; i < len(chars); i++ {
sort.Slice(chars, func(i int, j int) bool {
c, err := rand.Read(buf)

// "should never happen" clauses
if err != nil {
panic(err)
}
if c != len(buf) || c != 2 {
panic(fmt.Sprintf("crypto rand read %d bytes, expected %d", c, len(buf)))
}

return buf[0] < buf[1]
})
}

return strings.Join(chars[:6], "")
}

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

return any_server.DecodeSigningKey(f)
}
49 changes: 49 additions & 0 deletions homeserver_interop/any_server/signing_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package any_server

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

"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
var err error

var errorStack error

// Try Synapse first, as the most popular
priv, keyVersion, err = synapse.DecodeSigningKey(key)
if err == nil {
return priv, keyVersion, 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
}
priv, keyVersion, err = dendrite.DecodeSigningKey(key)
if err == nil {
return priv, keyVersion, 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
}
priv, keyVersion, err = mmr.DecodeSigningKey(key)
if err == nil {
return priv, keyVersion, 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)
}
57 changes: 57 additions & 0 deletions homeserver_interop/dendrite/signing_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dendrite

import (
"bytes"
"crypto/ed25519"
"encoding/pem"
"fmt"
"io"
"strings"
)

const blockType = "MATRIX PRIVATE KEY"

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

func DecodeSigningKey(key io.Reader) (ed25519.PrivateKey, string, error) {
b, err := io.ReadAll(key)
if err != nil {
return nil, "", err
}

var block *pem.Block
for {
block, b = pem.Decode(b)
if b == nil {
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.Type == blockType {
keyId := block.Headers["Key-ID"]
if len(keyId) <= 0 {
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)
}

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

return priv, keyId[len("ed25519:"):], nil
}
}
}
63 changes: 63 additions & 0 deletions homeserver_interop/mmr/signing_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package mmr

import (
"bytes"
"crypto/ed25519"
"encoding/pem"
"fmt"
"io"
"strings"
)

const blockType = "MMR PRIVATE KEY"

func EncodeSigningKey(keyVersion string, key ed25519.PrivateKey) ([]byte, error) {
// Similar to Dendrite, but using a different block type and added Version header for future expansion
block := &pem.Block{
Type: blockType,
Headers: map[string]string{
"Key-ID": fmt.Sprintf("ed25519:%s", keyVersion),
"Version": "1",
},
Bytes: key.Seed(),
}
return pem.EncodeToMemory(block), nil
}

func DecodeSigningKey(key io.Reader) (ed25519.PrivateKey, string, error) {
b, err := io.ReadAll(key)
if err != nil {
return nil, "", err
}

var block *pem.Block
for {
block, b = pem.Decode(b)
if b == nil {
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.Type == blockType {
version := block.Headers["Version"]
if version != "1" {
return nil, "", fmt.Errorf("unsupported MMR key format version")
}

keyId := block.Headers["Key-ID"]
if len(keyId) <= 0 {
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)
}
_, priv, err := ed25519.GenerateKey(bytes.NewReader(block.Bytes))
if err != nil {
return nil, "", err
}

return priv, keyId[len("ed25519:"):], nil
}
}
}
49 changes: 49 additions & 0 deletions homeserver_interop/synapse/signing_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package synapse

import (
"bytes"
"crypto/ed25519"
"errors"
"fmt"
"io"
"strings"

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

func EncodeSigningKey(keyVersion string, key ed25519.PrivateKey) ([]byte, error) {
b64 := util.EncodeUnpaddedBase64ToString(key.Seed())
return []byte(fmt.Sprintf("ed25519 %s %s", keyVersion, b64)), nil
}

func DecodeSigningKey(key io.Reader) (ed25519.PrivateKey, string, error) {
b, err := io.ReadAll(key)
if err != nil {
return nil, "", err
}

// See https://github.com/matrix-org/python-signedjson/blob/067ae81616573e8ceb627cc046d91b5b489bcc96/signedjson/key.py#L137-L150
parts := strings.Split(string(b), " ")
if len(parts) != 3 {
return nil, "", fmt.Errorf("expected 3 parts to signing key, got %d", len(parts))
}

if parts[0] != "ed25519" {
return nil, "", fmt.Errorf("expected ed25519 signing key, got '%s'", parts[0])
}

keyVersion := parts[1]
keyB64 := parts[2]

keyBytes, err := util.DecodeUnpaddedBase64String(keyB64)
if err != nil {
return nil, "", errors.Join(errors.New("expected base64 signing key part"), err)
}

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

return priv, keyVersion, nil
}
Loading