-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add signing key generation and conversion tooling (#512)
* 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
Showing
14 changed files
with
686 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
*.key | ||
/webui | ||
/.idea | ||
/bin | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
*.key | ||
/webui | ||
/.idea | ||
/bin | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.