Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
package x25519
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
)
|
||||
|
||||
// suiteID is the scheme identifier for X25519 ECDH KEM.
|
||||
const suiteID uint16 = 0x0007
|
||||
|
||||
// ErrDecapsulationFailed is returned when ciphertext decapsulation fails,
|
||||
// typically because the ciphertext is not a valid X25519 public key.
|
||||
var ErrDecapsulationFailed = errors.New("decapsulation failed")
|
||||
|
||||
// DefaultRegistry is the package-level registry for X25519.
|
||||
var DefaultRegistry = crypto.NewRegistry()
|
||||
|
||||
// kemAdapter wraps the Go stdlib crypto/ecdh X25519 implementation to satisfy
|
||||
// the pkg/domain/crypto.KEM interface.
|
||||
type kemAdapter struct{}
|
||||
|
||||
// New creates a new KEM adapter instance.
|
||||
func New() crypto.KEM {
|
||||
return &kemAdapter{}
|
||||
}
|
||||
|
||||
// SchemeID returns the X25519 scheme identifier (0x0007).
|
||||
func (k *kemAdapter) SchemeID() uint16 {
|
||||
return suiteID
|
||||
}
|
||||
|
||||
// GenerateKeyPair generates a new X25519 key pair.
|
||||
func (k *kemAdapter) GenerateKeyPair(
|
||||
rand io.Reader,
|
||||
) (
|
||||
crypto.RecipientPub,
|
||||
crypto.RecipientPriv,
|
||||
error,
|
||||
) {
|
||||
ecdhPriv, err := ecdh.X25519().GenerateKey(rand)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rawPub := ecdhPriv.PublicKey().Bytes()
|
||||
keyID := computeKeyID(rawPub)
|
||||
|
||||
pub := &pubKey{
|
||||
key: ecdhPriv.PublicKey(),
|
||||
keyID: keyID,
|
||||
}
|
||||
priv := &privKey{
|
||||
key: ecdhPriv,
|
||||
keyID: keyID,
|
||||
}
|
||||
|
||||
return pub, priv, nil
|
||||
}
|
||||
|
||||
// Encapsulate generates a shared secret and ciphertext for the given public key.
|
||||
// The ciphertext is the ephemeral public key (32 bytes).
|
||||
func (k *kemAdapter) Encapsulate(
|
||||
pub crypto.RecipientPub,
|
||||
rand io.Reader,
|
||||
) (
|
||||
ciphertext []byte,
|
||||
sharedSecret []byte,
|
||||
err error,
|
||||
) {
|
||||
p, ok := pub.(*pubKey)
|
||||
if !ok {
|
||||
raw := pub.Raw()
|
||||
ek, parseErr := ecdh.X25519().NewPublicKey(raw)
|
||||
if parseErr != nil {
|
||||
return nil, nil, fmt.Errorf("invalid public key for X25519: %w", parseErr)
|
||||
}
|
||||
p = &pubKey{key: ek, keyID: computeKeyID(raw)}
|
||||
}
|
||||
|
||||
ephPriv, err := ecdh.X25519().GenerateKey(rand)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ct := ephPriv.PublicKey().Bytes()
|
||||
ss, err := ephPriv.ECDH(p.key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ct, ss, nil
|
||||
}
|
||||
|
||||
// LoadPriv loads an X25519 private key from raw bytes.
|
||||
func (k *kemAdapter) LoadPriv(
|
||||
raw []byte,
|
||||
) (
|
||||
crypto.RecipientPriv,
|
||||
error,
|
||||
) {
|
||||
dk, err := ecdh.X25519().NewPrivateKey(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid X25519 private key: %w", err)
|
||||
}
|
||||
pubRaw := dk.PublicKey().Bytes()
|
||||
return &privKey{key: dk, keyID: computeKeyID(pubRaw)}, nil
|
||||
}
|
||||
|
||||
// Decapsulate recovers the shared secret from a ciphertext using the private key.
|
||||
// The ciphertext must be a valid 32-byte X25519 public key.
|
||||
func (k *kemAdapter) Decapsulate(
|
||||
priv crypto.RecipientPriv,
|
||||
ciphertext []byte,
|
||||
) (
|
||||
sharedSecret []byte,
|
||||
err error,
|
||||
) {
|
||||
p, ok := priv.(*privKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid private key type for X25519")
|
||||
}
|
||||
|
||||
if len(ciphertext) != 32 {
|
||||
return nil, errors.Join(ErrDecapsulationFailed, errors.New("invalid ciphertext length"))
|
||||
}
|
||||
|
||||
// X25519 public keys are 255-bit Montgomery u-coordinates; bit 255 must be zero.
|
||||
if ciphertext[31]&0x80 != 0 {
|
||||
return nil, ErrDecapsulationFailed
|
||||
}
|
||||
|
||||
// Reject the all-zero public key (identity point), which yields an all-zero shared secret.
|
||||
allZero := true
|
||||
for _, b := range ciphertext {
|
||||
if b != 0 {
|
||||
allZero = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allZero {
|
||||
return nil, ErrDecapsulationFailed
|
||||
}
|
||||
|
||||
ephPub, err := ecdh.X25519().NewPublicKey(ciphertext)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrDecapsulationFailed, err)
|
||||
}
|
||||
|
||||
ss, err := p.key.ECDH(ephPub)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrDecapsulationFailed, err)
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// pubKey wraps *ecdh.PublicKey to satisfy crypto.RecipientPub.
|
||||
type pubKey struct {
|
||||
key *ecdh.PublicKey
|
||||
keyID []byte
|
||||
}
|
||||
|
||||
func (p *pubKey) SchemeID() uint16 { return suiteID }
|
||||
func (p *pubKey) KeyID() []byte { return p.keyID }
|
||||
func (p *pubKey) Raw() []byte { return p.key.Bytes() }
|
||||
|
||||
// privKey wraps *ecdh.PrivateKey to satisfy crypto.RecipientPriv.
|
||||
type privKey struct {
|
||||
key *ecdh.PrivateKey
|
||||
keyID []byte
|
||||
}
|
||||
|
||||
func (p *privKey) SchemeID() uint16 { return suiteID }
|
||||
func (p *privKey) KeyID() []byte { return p.keyID }
|
||||
func (p *privKey) Raw() []byte { return p.key.Bytes() }
|
||||
|
||||
// computeKeyID derives the first 8 bytes of SHA-256 over the raw public key.
|
||||
func computeKeyID(raw []byte) []byte {
|
||||
h := sha256.Sum256(raw)
|
||||
return h[:8]
|
||||
}
|
||||
|
||||
// init registers the X25519 factory under suiteID 0x0007.
|
||||
func init() {
|
||||
_ = DefaultRegistry.Register(
|
||||
suiteID,
|
||||
func() crypto.KEM {
|
||||
return New()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package x25519
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateKeyPair(t *testing.T) {
|
||||
adapter := New()
|
||||
|
||||
pub, priv, err := adapter.GenerateKeyPair(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
if pub.SchemeID() != suiteID {
|
||||
t.Errorf("pub.SchemeID() = 0x%04x, want 0x%04x", pub.SchemeID(), suiteID)
|
||||
}
|
||||
|
||||
if priv.SchemeID() != suiteID {
|
||||
t.Errorf("priv.SchemeID() = 0x%04x, want 0x%04x", priv.SchemeID(), suiteID)
|
||||
}
|
||||
|
||||
rawPub := pub.Raw()
|
||||
if len(rawPub) != 32 {
|
||||
t.Errorf("pub.Raw() len = %d, want 32", len(rawPub))
|
||||
}
|
||||
|
||||
rawPriv := priv.Raw()
|
||||
if len(rawPriv) != 32 {
|
||||
t.Errorf("priv.Raw() len = %d, want 32", len(rawPriv))
|
||||
}
|
||||
|
||||
expectedKeyID := sha256.Sum256(rawPub)
|
||||
if !bytes.Equal(pub.KeyID(), expectedKeyID[:8]) {
|
||||
t.Errorf("pub.KeyID() = %x, want %x", pub.KeyID(), expectedKeyID[:8])
|
||||
}
|
||||
|
||||
if !bytes.Equal(priv.KeyID(), pub.KeyID()) {
|
||||
t.Errorf("priv.KeyID() = %x, want %x", priv.KeyID(), pub.KeyID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncapsulate(t *testing.T) {
|
||||
adapter := New()
|
||||
|
||||
pub, priv, err := adapter.GenerateKeyPair(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
ct, ss, err := adapter.Encapsulate(pub, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Encapsulate failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ct) != 32 {
|
||||
t.Errorf("ciphertext len = %d, want 32", len(ct))
|
||||
}
|
||||
|
||||
if len(ss) != 32 {
|
||||
t.Errorf("sharedSecret len = %d, want 32", len(ss))
|
||||
}
|
||||
|
||||
// Verify ss by independently computing priv.ECDH(ephemeralPubParsedFromCt).
|
||||
ephPub, err := ecdh.X25519().NewPublicKey(ct)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse ephemeral public key from ciphertext: %v", err)
|
||||
}
|
||||
|
||||
parsedPriv, err := ecdh.X25519().NewPrivateKey(priv.Raw())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse private key: %v", err)
|
||||
}
|
||||
|
||||
computedSS, err := parsedPriv.ECDH(ephPub)
|
||||
if err != nil {
|
||||
t.Fatalf("independent ECDH computation failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ss, computedSS) {
|
||||
t.Errorf("shared secret mismatch: encapsulate=%x, independent=%x", ss, computedSS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
adapter := New()
|
||||
|
||||
pub, priv, err := adapter.GenerateKeyPair(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
ct, ssEnc, err := adapter.Encapsulate(pub, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Encapsulate failed: %v", err)
|
||||
}
|
||||
|
||||
ssDec, err := adapter.Decapsulate(priv, ct)
|
||||
if err != nil {
|
||||
t.Fatalf("Decapsulate failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ssEnc, ssDec) {
|
||||
t.Fatalf("shared secret mismatch: encapsulate=%x, decapsulate=%x", ssEnc, ssDec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripMany(t *testing.T) {
|
||||
adapter := New()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
pub, priv, err := adapter.GenerateKeyPair(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d: GenerateKeyPair failed: %v", i, err)
|
||||
}
|
||||
|
||||
ct, ssEnc, err := adapter.Encapsulate(pub, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d: Encapsulate failed: %v", i, err)
|
||||
}
|
||||
|
||||
ssDec, err := adapter.Decapsulate(priv, ct)
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d: Decapsulate failed: %v", i, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ssEnc, ssDec) {
|
||||
t.Fatalf("iteration %d: shared secret mismatch", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecapsulateRandomCiphertext(t *testing.T) {
|
||||
adapter := New()
|
||||
|
||||
_, priv, err := adapter.GenerateKeyPair(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
// Generate a random 32-byte string that is unlikely to be a valid X25519 public key.
|
||||
// Setting the high bit makes it invalid for X25519 (Montgomery u-coordinate must be < 2^255).
|
||||
randomCT := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, randomCT); err != nil {
|
||||
t.Fatalf("failed to read random bytes: %v", err)
|
||||
}
|
||||
randomCT[31] |= 0x80 // set high bit to guarantee invalidity
|
||||
|
||||
_, err = adapter.Decapsulate(priv, randomCT)
|
||||
if err == nil {
|
||||
t.Fatal("Decapsulate with random ciphertext: expected error, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, ErrDecapsulationFailed) {
|
||||
t.Errorf("Decapsulate error = %v, want ErrDecapsulationFailed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryRegistration(t *testing.T) {
|
||||
factory, err := DefaultRegistry.Lookup(suiteID)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup suiteID 0x%04x failed: %v", suiteID, err)
|
||||
}
|
||||
|
||||
instance := factory()
|
||||
if instance.SchemeID() != suiteID {
|
||||
t.Errorf("factory() SchemeID = 0x%04x, want 0x%04x", instance.SchemeID(), suiteID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactoryReturnsIndependentInstances(t *testing.T) {
|
||||
factory, err := DefaultRegistry.Lookup(suiteID)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup suiteID 0x%04x failed: %v", suiteID, err)
|
||||
}
|
||||
|
||||
one := factory()
|
||||
two := factory()
|
||||
|
||||
if one.SchemeID() != two.SchemeID() {
|
||||
t.Error("factory() returned instances with different scheme IDs")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user