Skip to content

Commit

Permalink
Add signing key generation and conversion tooling (#512)
Browse files Browse the repository at this point in the history
* Add signing key generation and conversion tooling

Will be used by MSC3916 implementation.

* Relicense my own utility as MIT

* also relicense canonical_json util

* appease the linter

* don't forget the relicensed test too
  • Loading branch information
turt2live authored Nov 26, 2023
1 parent a361f1e commit 5a676a6
Show file tree
Hide file tree
Showing 14 changed files with 686 additions and 0 deletions.
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

0 comments on commit 5a676a6

Please sign in to comment.