Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package keymanager
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPEMTypeMismatch is returned when the PEM block type does not match
|
||||
// the expected type for the given schemeID.
|
||||
ErrPEMTypeMismatch = errors.New("PEM type does not match scheme")
|
||||
// ErrInvalidPEM is returned when the file does not contain a valid PEM block.
|
||||
ErrInvalidPEM = errors.New("invalid PEM data")
|
||||
)
|
||||
|
||||
var pubPEMTypes = map[uint16]string{
|
||||
0x0006: "ML-KEM-768 PUBLIC KEY",
|
||||
0x0007: "X25519 PUBLIC KEY",
|
||||
}
|
||||
|
||||
var privPEMTypes = map[uint16]string{
|
||||
0x0006: "ML-KEM-768 PRIVATE KEY",
|
||||
0x0007: "X25519 PRIVATE KEY",
|
||||
}
|
||||
|
||||
// keyManager handles PEM encoding and decoding of recipient keys.
|
||||
type keyManager struct {
|
||||
registry crypto.Registry
|
||||
}
|
||||
|
||||
// NewKeyManager creates a new KeyManager backed by the provided Registry.
|
||||
func NewKeyManager(
|
||||
registry crypto.Registry,
|
||||
) crypto.KeyManager {
|
||||
return &keyManager{
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate creates a new key pair for the given schemeID and writes them
|
||||
// as PEM blocks to pubOut and privOut.
|
||||
func (k *keyManager) Generate(
|
||||
schemeID uint16,
|
||||
pubOut io.Writer,
|
||||
privOut io.Writer,
|
||||
rand io.Reader,
|
||||
) error {
|
||||
factory, err := k.registry.Lookup(schemeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kem := factory()
|
||||
pub, priv, err := kem.GenerateKeyPair(rand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubType, ok := pubPEMTypes[schemeID]
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"unsupported scheme 0x%04x for public key PEM",
|
||||
schemeID,
|
||||
)
|
||||
}
|
||||
|
||||
privType, ok := privPEMTypes[schemeID]
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"unsupported scheme 0x%04x for private key PEM",
|
||||
schemeID,
|
||||
)
|
||||
}
|
||||
|
||||
pubBlock := &pem.Block{
|
||||
Type: pubType,
|
||||
Bytes: pub.Raw(),
|
||||
}
|
||||
if err := pem.Encode(pubOut, pubBlock); err != nil {
|
||||
return fmt.Errorf("encode public key PEM: %w", err)
|
||||
}
|
||||
|
||||
privBlock := &pem.Block{
|
||||
Type: privType,
|
||||
Bytes: priv.Raw(),
|
||||
}
|
||||
if err := pem.Encode(privOut, privBlock); err != nil {
|
||||
return fmt.Errorf("encode private key PEM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPub reads a PEM-encoded public key from path and validates that its
|
||||
// type matches the expected type for schemeID.
|
||||
func (k *keyManager) LoadPub(
|
||||
path string,
|
||||
schemeID uint16,
|
||||
) (
|
||||
crypto.RecipientPub,
|
||||
error,
|
||||
) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read public key file: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("%w: no valid PEM block found", ErrInvalidPEM)
|
||||
}
|
||||
|
||||
expectedType, ok := pubPEMTypes[schemeID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported scheme 0x%04x for public key PEM",
|
||||
schemeID,
|
||||
)
|
||||
}
|
||||
|
||||
if block.Type != expectedType {
|
||||
return nil, fmt.Errorf(
|
||||
"expected PEM type %q, got %q: %w",
|
||||
expectedType,
|
||||
block.Type,
|
||||
ErrPEMTypeMismatch,
|
||||
)
|
||||
}
|
||||
|
||||
return newRecipientPub(schemeID, block.Bytes), nil
|
||||
}
|
||||
|
||||
// LoadPriv reads a PEM-encoded private key from path and validates that its
|
||||
// type matches the expected type for schemeID.
|
||||
func (k *keyManager) LoadPriv(
|
||||
path string,
|
||||
schemeID uint16,
|
||||
) (
|
||||
crypto.RecipientPriv,
|
||||
error,
|
||||
) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read private key file: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("%w: no valid PEM block found", ErrInvalidPEM)
|
||||
}
|
||||
|
||||
expectedType, ok := privPEMTypes[schemeID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported scheme 0x%04x for private key PEM",
|
||||
schemeID,
|
||||
)
|
||||
}
|
||||
|
||||
if block.Type != expectedType {
|
||||
return nil, fmt.Errorf(
|
||||
"expected PEM type %q, got %q: %w",
|
||||
expectedType,
|
||||
block.Type,
|
||||
ErrPEMTypeMismatch,
|
||||
)
|
||||
}
|
||||
|
||||
factory, err := k.registry.Lookup(schemeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kem := factory()
|
||||
return kem.LoadPriv(block.Bytes)
|
||||
}
|
||||
|
||||
// recipientPub is a generic RecipientPub implementation backed by raw bytes.
|
||||
type recipientPub struct {
|
||||
schemeID uint16
|
||||
raw []byte
|
||||
keyID []byte
|
||||
}
|
||||
|
||||
func newRecipientPub(
|
||||
schemeID uint16,
|
||||
raw []byte,
|
||||
) crypto.RecipientPub {
|
||||
var keyID []byte
|
||||
|
||||
switch schemeID {
|
||||
case 0x0006:
|
||||
h := sha256.Sum256(raw[:8])
|
||||
keyID = h[:8]
|
||||
case 0x0007:
|
||||
h := sha256.Sum256(raw)
|
||||
keyID = h[:8]
|
||||
}
|
||||
|
||||
return &recipientPub{
|
||||
schemeID: schemeID,
|
||||
raw: append([]byte(nil), raw...),
|
||||
keyID: keyID,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recipientPub) SchemeID() uint16 { return r.schemeID }
|
||||
func (r *recipientPub) KeyID() []byte { return r.keyID }
|
||||
func (r *recipientPub) Raw() []byte { return r.raw }
|
||||
@@ -0,0 +1,362 @@
|
||||
package keymanager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
)
|
||||
|
||||
func makeRegistry(
|
||||
t *testing.T,
|
||||
) crypto.Registry {
|
||||
reg := crypto.NewRegistry()
|
||||
|
||||
if err := reg.Register(
|
||||
0x0006,
|
||||
func() crypto.KEM {
|
||||
return mlkem768.New()
|
||||
},
|
||||
); err != nil {
|
||||
t.Fatalf("register mlkem768: %v", err)
|
||||
}
|
||||
|
||||
if err := reg.Register(
|
||||
0x0007,
|
||||
func() crypto.KEM {
|
||||
return x25519.New()
|
||||
},
|
||||
); err != nil {
|
||||
t.Fatalf("register x25519: %v", err)
|
||||
}
|
||||
|
||||
return reg
|
||||
}
|
||||
|
||||
func TestGenerateMLKEM768(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
var pubOut, privOut bytes.Buffer
|
||||
|
||||
err := km.Generate(
|
||||
0x0006,
|
||||
&pubOut,
|
||||
&privOut,
|
||||
rand.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate failed: %v", err)
|
||||
}
|
||||
|
||||
pubBlock, _ := pem.Decode(pubOut.Bytes())
|
||||
if pubBlock == nil {
|
||||
t.Fatal("failed to decode public key PEM")
|
||||
}
|
||||
if pubBlock.Type != "ML-KEM-768 PUBLIC KEY" {
|
||||
t.Errorf("pub PEM type = %q, want %q", pubBlock.Type, "ML-KEM-768 PUBLIC KEY")
|
||||
}
|
||||
if len(pubBlock.Bytes) != 1184 {
|
||||
t.Errorf("pub raw len = %d, want 1184", len(pubBlock.Bytes))
|
||||
}
|
||||
|
||||
privBlock, _ := pem.Decode(privOut.Bytes())
|
||||
if privBlock == nil {
|
||||
t.Fatal("failed to decode private key PEM")
|
||||
}
|
||||
if privBlock.Type != "ML-KEM-768 PRIVATE KEY" {
|
||||
t.Errorf("priv PEM type = %q, want %q", privBlock.Type, "ML-KEM-768 PRIVATE KEY")
|
||||
}
|
||||
if len(privBlock.Bytes) != 64 {
|
||||
t.Errorf("priv raw len = %d, want 64", len(privBlock.Bytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateX25519(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
var pubOut, privOut bytes.Buffer
|
||||
|
||||
err := km.Generate(
|
||||
0x0007,
|
||||
&pubOut,
|
||||
&privOut,
|
||||
rand.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate failed: %v", err)
|
||||
}
|
||||
|
||||
pubBlock, _ := pem.Decode(pubOut.Bytes())
|
||||
if pubBlock == nil {
|
||||
t.Fatal("failed to decode public key PEM")
|
||||
}
|
||||
if pubBlock.Type != "X25519 PUBLIC KEY" {
|
||||
t.Errorf("pub PEM type = %q, want %q", pubBlock.Type, "X25519 PUBLIC KEY")
|
||||
}
|
||||
if len(pubBlock.Bytes) != 32 {
|
||||
t.Errorf("pub raw len = %d, want 32", len(pubBlock.Bytes))
|
||||
}
|
||||
|
||||
privBlock, _ := pem.Decode(privOut.Bytes())
|
||||
if privBlock == nil {
|
||||
t.Fatal("failed to decode private key PEM")
|
||||
}
|
||||
if privBlock.Type != "X25519 PRIVATE KEY" {
|
||||
t.Errorf("priv PEM type = %q, want %q", privBlock.Type, "X25519 PRIVATE KEY")
|
||||
}
|
||||
if len(privBlock.Bytes) != 32 {
|
||||
t.Errorf("priv raw len = %d, want 32", len(privBlock.Bytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPubMLKEM768(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
var pubOut, privOut bytes.Buffer
|
||||
|
||||
err := km.Generate(
|
||||
0x0006,
|
||||
&pubOut,
|
||||
&privOut,
|
||||
rand.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate failed: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
pubPath := filepath.Join(dir, "test.pub.pem")
|
||||
|
||||
if err := os.WriteFile(pubPath, pubOut.Bytes(), 0o644); err != nil {
|
||||
t.Fatalf("write pub file: %v", err)
|
||||
}
|
||||
|
||||
pub, err := km.LoadPub(pubPath, 0x0006)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPub failed: %v", err)
|
||||
}
|
||||
if pub.SchemeID() != 0x0006 {
|
||||
t.Errorf("pub.SchemeID() = 0x%04x, want 0x0006", pub.SchemeID())
|
||||
}
|
||||
if len(pub.Raw()) != 1184 {
|
||||
t.Errorf("pub.Raw() len = %d, want 1184", len(pub.Raw()))
|
||||
}
|
||||
|
||||
expectedKeyID := sha256.Sum256(pub.Raw()[:8])
|
||||
if !bytes.Equal(pub.KeyID(), expectedKeyID[:8]) {
|
||||
t.Errorf("pub.KeyID() = %x, want %x", pub.KeyID(), expectedKeyID[:8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrivMLKEM768(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
var pubOut, privOut bytes.Buffer
|
||||
|
||||
err := km.Generate(
|
||||
0x0006,
|
||||
&pubOut,
|
||||
&privOut,
|
||||
rand.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate failed: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
privPath := filepath.Join(dir, "test.priv.pem")
|
||||
|
||||
if err := os.WriteFile(privPath, privOut.Bytes(), 0o600); err != nil {
|
||||
t.Fatalf("write priv file: %v", err)
|
||||
}
|
||||
|
||||
priv, err := km.LoadPriv(privPath, 0x0006)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPriv failed: %v", err)
|
||||
}
|
||||
if priv.SchemeID() != 0x0006 {
|
||||
t.Errorf("priv.SchemeID() = 0x%04x, want 0x0006", priv.SchemeID())
|
||||
}
|
||||
if len(priv.Raw()) != 64 {
|
||||
t.Errorf("priv.Raw() len = %d, want 64", len(priv.Raw()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPubX25519(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
var pubOut, privOut bytes.Buffer
|
||||
|
||||
err := km.Generate(
|
||||
0x0007,
|
||||
&pubOut,
|
||||
&privOut,
|
||||
rand.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate failed: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
pubPath := filepath.Join(dir, "test.pub.pem")
|
||||
|
||||
if err := os.WriteFile(pubPath, pubOut.Bytes(), 0o644); err != nil {
|
||||
t.Fatalf("write pub file: %v", err)
|
||||
}
|
||||
|
||||
pub, err := km.LoadPub(pubPath, 0x0007)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPub failed: %v", err)
|
||||
}
|
||||
if pub.SchemeID() != 0x0007 {
|
||||
t.Errorf("pub.SchemeID() = 0x%04x, want 0x0007", pub.SchemeID())
|
||||
}
|
||||
if len(pub.Raw()) != 32 {
|
||||
t.Errorf("pub.Raw() len = %d, want 32", len(pub.Raw()))
|
||||
}
|
||||
|
||||
expectedKeyID := sha256.Sum256(pub.Raw())
|
||||
if !bytes.Equal(pub.KeyID(), expectedKeyID[:8]) {
|
||||
t.Errorf("pub.KeyID() = %x, want %x", pub.KeyID(), expectedKeyID[:8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrivX25519(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
var pubOut, privOut bytes.Buffer
|
||||
|
||||
err := km.Generate(
|
||||
0x0007,
|
||||
&pubOut,
|
||||
&privOut,
|
||||
rand.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate failed: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
privPath := filepath.Join(dir, "test.priv.pem")
|
||||
|
||||
if err := os.WriteFile(privPath, privOut.Bytes(), 0o600); err != nil {
|
||||
t.Fatalf("write priv file: %v", err)
|
||||
}
|
||||
|
||||
priv, err := km.LoadPriv(privPath, 0x0007)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPriv failed: %v", err)
|
||||
}
|
||||
if priv.SchemeID() != 0x0007 {
|
||||
t.Errorf("priv.SchemeID() = 0x%04x, want 0x0007", priv.SchemeID())
|
||||
}
|
||||
if len(priv.Raw()) != 32 {
|
||||
t.Errorf("priv.Raw() len = %d, want 32", len(priv.Raw()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPubWrongPEMType(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
dir := t.TempDir()
|
||||
pubPath := filepath.Join(dir, "wrong.pub.pem")
|
||||
|
||||
block := &pem.Block{
|
||||
Type: "X25519 PUBLIC KEY",
|
||||
Bytes: make([]byte, 32),
|
||||
}
|
||||
data := pem.EncodeToMemory(block)
|
||||
|
||||
if err := os.WriteFile(pubPath, data, 0o644); err != nil {
|
||||
t.Fatalf("write pub file: %v", err)
|
||||
}
|
||||
|
||||
_, err := km.LoadPub(pubPath, 0x0006)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong PEM type")
|
||||
}
|
||||
if !errors.Is(err, ErrPEMTypeMismatch) {
|
||||
t.Errorf("error = %v, want ErrPEMTypeMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrivWrongPEMType(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
dir := t.TempDir()
|
||||
privPath := filepath.Join(dir, "wrong.priv.pem")
|
||||
|
||||
block := &pem.Block{
|
||||
Type: "ML-KEM-768 PRIVATE KEY",
|
||||
Bytes: make([]byte, 64),
|
||||
}
|
||||
data := pem.EncodeToMemory(block)
|
||||
|
||||
if err := os.WriteFile(privPath, data, 0o600); err != nil {
|
||||
t.Fatalf("write priv file: %v", err)
|
||||
}
|
||||
|
||||
_, err := km.LoadPriv(privPath, 0x0007)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong PEM type")
|
||||
}
|
||||
if !errors.Is(err, ErrPEMTypeMismatch) {
|
||||
t.Errorf("error = %v, want ErrPEMTypeMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPubTruncatedPEM(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
dir := t.TempDir()
|
||||
pubPath := filepath.Join(dir, "truncated.pub.pem")
|
||||
|
||||
if err := os.WriteFile(pubPath, []byte("-----BEGIN ML-KEM-768 PUBLIC KEY-----\nnotbase64\n"), 0o644); err != nil {
|
||||
t.Fatalf("write pub file: %v", err)
|
||||
}
|
||||
|
||||
_, err := km.LoadPub(pubPath, 0x0006)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for truncated PEM")
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidPEM) {
|
||||
t.Errorf("error = %v, want ErrInvalidPEM", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrivTruncatedPEM(t *testing.T) {
|
||||
reg := makeRegistry(t)
|
||||
km := NewKeyManager(reg)
|
||||
|
||||
dir := t.TempDir()
|
||||
privPath := filepath.Join(dir, "truncated.priv.pem")
|
||||
|
||||
if err := os.WriteFile(privPath, []byte("-----BEGIN X25519 PRIVATE KEY-----\nnotbase64\n"), 0o600); err != nil {
|
||||
t.Fatalf("write priv file: %v", err)
|
||||
}
|
||||
|
||||
_, err := km.LoadPriv(privPath, 0x0007)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for truncated PEM")
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidPEM) {
|
||||
t.Errorf("error = %v, want ErrInvalidPEM", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user