diff --git a/README.md b/README.md index 422fc1f..baa9bf6 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ go get github.com/AeonDave/cryptonite-go ### Public Key Crypto - **Signatures**: Ed25519, ECDSA P-256 -- **Key Exchange**: X25519, ECDH P-256/P-384 +- **Key Exchange**: X25519, X448, ECDH P-256/P-384 - **Post-Quantum**: Hybrid X25519+ML-KEM ready (via `pq` package) Full algorithm matrix with specs: @@ -88,18 +88,24 @@ digest := hasher.Hash([]byte("hello world")) fmt.Printf("%x\n", digest) ``` -### Key Exchange (X25519) +### Key Exchange (X25519 / X448) ```go import "github.com/AeonDave/cryptonite-go/ecdh" x25519 := ecdh.NewX25519() +x448 := ecdh.NewX448() alicePriv, _ := x25519.GenerateKey() bobPriv, _ := x25519.GenerateKey() aliceShared, _ := x25519.SharedSecret(alicePriv, bobPriv.PublicKey()) bobShared, _ := x25519.SharedSecret(bobPriv, alicePriv.PublicKey()) // aliceShared == bobShared + +// X448 exposes the same API for higher security deployments. +alice448, _ := x448.GenerateKey() +bob448, _ := x448.GenerateKey() +shared448, _ := x448.SharedSecret(alice448, bob448.PublicKey()) ``` ### Digital Signatures (Ed25519) diff --git a/docs/ALGORITHMS.md b/docs/ALGORITHMS.md index 0b9c32c..f5f6242 100644 --- a/docs/ALGORITHMS.md +++ b/docs/ALGORITHMS.md @@ -114,6 +114,7 @@ sites. | Algorithm | Constructor | Public | Private | Shared | Notes | RFC / Spec | |-----------|--------------------|--------------------|------------|--------|---------------------------|---------------------------------------------------------| | X25519 | `ecdh.NewX25519()` | 32B | 32B | 32B | RFC 7748 (crypto/ecdh) | [RFC 7748](https://www.rfc-editor.org/rfc/rfc7748.html) | +| X448 | `ecdh.NewX448()` | 56B | 56B | 56B | RFC 7748 (pure Go impl.) | [RFC 7748](https://www.rfc-editor.org/rfc/rfc7748.html) | | P-256 | `ecdh.NewP256()` | 65B (uncompressed) | 32B scalar | 32B | Uncompressed public: 0x04 | | X || Y | [FIPS 186-5](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf) | | P-384 | `ecdh.NewP384()` | 97B (uncompressed) | 48B scalar | 48B | Uncompressed public: 0x04 | | X || Y | [FIPS 186-5](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf) | diff --git a/docs/INTEROP.md b/docs/INTEROP.md index d2c924d..fa6c29b 100644 --- a/docs/INTEROP.md +++ b/docs/INTEROP.md @@ -21,6 +21,8 @@ and compatibility notes. - `ecdh.NewX25519` expects 32-byte private keys and outputs 32-byte Montgomery u-coordinates. Use the standard base point defined in RFC 7748. +- `ecdh.NewX448` expects 56-byte private scalars and emits 56-byte Montgomery u-coordinates. The canonical base point is + the little-endian encoding of the integer 5. - `ecdh.NewP256` / `NewP384` accept scalar private keys and return uncompressed SEC1 public points. ## HPKE Records diff --git a/ecdh/ecdh.go b/ecdh/ecdh.go index 78866e0..e3f1995 100644 --- a/ecdh/ecdh.go +++ b/ecdh/ecdh.go @@ -1,27 +1,62 @@ package ecdh import ( + "crypto" stdecdh "crypto/ecdh" "crypto/rand" + "errors" ) +// PrivateKey represents an ECDH private key backed either by the Go standard +// library implementation or by a custom curve implementation (such as X448). +// +// The interface intentionally mirrors the small subset of methods exposed by +// crypto/ecdh.PrivateKey that are required across the repository. This allows +// callers to operate on keys uniformly without leaking the concrete +// implementation details or exposing mutable internal buffers. +type PrivateKey interface { + // Bytes returns the canonical encoding of the private key. + Bytes() []byte + // PublicKey returns the corresponding public key instance. + PublicKey() PublicKey + // ECDH computes the shared secret with the peer public key. + ECDH(peer PublicKey) ([]byte, error) + // Equal reports whether the provided key matches this private key. + Equal(x crypto.PrivateKey) bool +} + +// PublicKey represents an ECDH public key suitable for the associated +// PrivateKey type. +type PublicKey interface { + // Bytes returns the canonical encoding of the public key. + Bytes() []byte + // Equal reports whether the provided key matches this public key. + Equal(x crypto.PublicKey) bool +} + // KeyExchange describes the minimal API shared by ECDH helpers exposed by the -// library. Implementations are thin wrappers around crypto/ecdh curves and -// provide uniform helpers for performing Diffie-Hellman operations without -// leaking the underlying curve-specific types to callers. +// library. Implementations may wrap crypto/ecdh curves or provide custom +// curve-specific logic while presenting a uniform surface to callers. type KeyExchange interface { - // Curve returns the underlying crypto/ecdh curve implementation. + // Curve returns the underlying crypto/ecdh curve implementation when + // available. For custom curves without a crypto/ecdh counterpart this may + // return nil. Curve() stdecdh.Curve // GenerateKey creates a new private key using crypto/rand. - GenerateKey() (*stdecdh.PrivateKey, error) + GenerateKey() (PrivateKey, error) // NewPrivateKey constructs a private key from scalar bytes. - NewPrivateKey(d []byte) (*stdecdh.PrivateKey, error) + NewPrivateKey(d []byte) (PrivateKey, error) // NewPublicKey parses a peer public key in the format required by the curve. - NewPublicKey(b []byte) (*stdecdh.PublicKey, error) + NewPublicKey(b []byte) (PublicKey, error) // SharedSecret performs the ECDH operation between private and peer. - SharedSecret(p *stdecdh.PrivateKey, peer *stdecdh.PublicKey) ([]byte, error) + SharedSecret(p PrivateKey, peer PublicKey) ([]byte, error) } +var ( + errIncompatiblePrivate = errors.New("ecdh: incompatible private key type") + errIncompatiblePublic = errors.New("ecdh: incompatible public key type") +) + type curveImpl struct { curve stdecdh.Curve } @@ -36,18 +71,92 @@ func NewKeyExchange(curve stdecdh.Curve) KeyExchange { func (c *curveImpl) Curve() stdecdh.Curve { return c.curve } -func (c *curveImpl) GenerateKey() (*stdecdh.PrivateKey, error) { - return c.curve.GenerateKey(rand.Reader) +func (c *curveImpl) GenerateKey() (PrivateKey, error) { + priv, err := c.curve.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + return &stdPrivateKey{key: priv}, nil +} + +func (c *curveImpl) NewPrivateKey(d []byte) (PrivateKey, error) { + priv, err := c.curve.NewPrivateKey(d) + if err != nil { + return nil, err + } + return &stdPrivateKey{key: priv}, nil +} + +func (c *curveImpl) NewPublicKey(b []byte) (PublicKey, error) { + pub, err := c.curve.NewPublicKey(b) + if err != nil { + return nil, err + } + return &stdPublicKey{key: pub}, nil +} + +func (c *curveImpl) SharedSecret(p PrivateKey, peer PublicKey) ([]byte, error) { + sp, ok := p.(*stdPrivateKey) + if !ok { + return nil, errIncompatiblePrivate + } + pp, ok := peer.(*stdPublicKey) + if !ok { + return nil, errIncompatiblePublic + } + return sp.key.ECDH(pp.key) +} + +type stdPrivateKey struct { + key *stdecdh.PrivateKey } -func (c *curveImpl) NewPrivateKey(d []byte) (*stdecdh.PrivateKey, error) { - return c.curve.NewPrivateKey(d) +func (k *stdPrivateKey) Bytes() []byte { + if k == nil || k.key == nil { + return nil + } + return k.key.Bytes() } -func (c *curveImpl) NewPublicKey(b []byte) (*stdecdh.PublicKey, error) { - return c.curve.NewPublicKey(b) +func (k *stdPrivateKey) PublicKey() PublicKey { + if k == nil || k.key == nil { + return nil + } + return &stdPublicKey{key: k.key.PublicKey()} } -func (c *curveImpl) SharedSecret(p *stdecdh.PrivateKey, peer *stdecdh.PublicKey) ([]byte, error) { - return p.ECDH(peer) +func (k *stdPrivateKey) ECDH(peer PublicKey) ([]byte, error) { + if k == nil || k.key == nil { + return nil, errors.New("ecdh: nil private key") + } + pp, ok := peer.(*stdPublicKey) + if !ok { + return nil, errIncompatiblePublic + } + return k.key.ECDH(pp.key) +} + +func (k *stdPrivateKey) Equal(x crypto.PrivateKey) bool { + if k == nil || k.key == nil { + return false + } + return k.key.Equal(x) +} + +type stdPublicKey struct { + key *stdecdh.PublicKey +} + +func (k *stdPublicKey) Bytes() []byte { + if k == nil || k.key == nil { + return nil + } + return k.key.Bytes() +} + +func (k *stdPublicKey) Equal(x crypto.PublicKey) bool { + if k == nil || k.key == nil { + return false + } + return k.key.Equal(x) } diff --git a/ecdh/p256.go b/ecdh/p256.go index 5166e1f..f1d2d54 100644 --- a/ecdh/p256.go +++ b/ecdh/p256.go @@ -16,15 +16,15 @@ func CurveP256() stdecdh.Curve { return p256Curve } func NewP256() KeyExchange { return p256Impl } // GenerateKeyP256 creates a new private key using crypto/rand. -func GenerateKeyP256() (*stdecdh.PrivateKey, error) { return p256Impl.GenerateKey() } +func GenerateKeyP256() (PrivateKey, error) { return p256Impl.GenerateKey() } // NewPrivateKeyP256 constructs a private key from scalar bytes. -func NewPrivateKeyP256(d []byte) (*stdecdh.PrivateKey, error) { return p256Impl.NewPrivateKey(d) } +func NewPrivateKeyP256(d []byte) (PrivateKey, error) { return p256Impl.NewPrivateKey(d) } // NewPublicKeyP256 parses an uncompressed public key. -func NewPublicKeyP256(b []byte) (*stdecdh.PublicKey, error) { return p256Impl.NewPublicKey(b) } +func NewPublicKeyP256(b []byte) (PublicKey, error) { return p256Impl.NewPublicKey(b) } // SharedSecretP256 performs the ECDH operation between private and peer. -func SharedSecretP256(p *stdecdh.PrivateKey, peer *stdecdh.PublicKey) ([]byte, error) { +func SharedSecretP256(p PrivateKey, peer PublicKey) ([]byte, error) { return p256Impl.SharedSecret(p, peer) } diff --git a/ecdh/p384.go b/ecdh/p384.go index 51acf4e..1efc702 100644 --- a/ecdh/p384.go +++ b/ecdh/p384.go @@ -16,15 +16,15 @@ func CurveP384() stdecdh.Curve { return p384Curve } func NewP384() KeyExchange { return p384Impl } // GenerateKeyP384 creates a new private key using crypto/rand. -func GenerateKeyP384() (*stdecdh.PrivateKey, error) { return p384Impl.GenerateKey() } +func GenerateKeyP384() (PrivateKey, error) { return p384Impl.GenerateKey() } // NewPrivateKeyP384 constructs a private key from scalar bytes. -func NewPrivateKeyP384(d []byte) (*stdecdh.PrivateKey, error) { return p384Impl.NewPrivateKey(d) } +func NewPrivateKeyP384(d []byte) (PrivateKey, error) { return p384Impl.NewPrivateKey(d) } // NewPublicKeyP384 parses an uncompressed public key. -func NewPublicKeyP384(b []byte) (*stdecdh.PublicKey, error) { return p384Impl.NewPublicKey(b) } +func NewPublicKeyP384(b []byte) (PublicKey, error) { return p384Impl.NewPublicKey(b) } // SharedSecretP384 performs the ECDH operation between private and peer. -func SharedSecretP384(p *stdecdh.PrivateKey, peer *stdecdh.PublicKey) ([]byte, error) { +func SharedSecretP384(p PrivateKey, peer PublicKey) ([]byte, error) { return p384Impl.SharedSecret(p, peer) } diff --git a/ecdh/x25519.go b/ecdh/x25519.go index f53faa0..86b1ac3 100644 --- a/ecdh/x25519.go +++ b/ecdh/x25519.go @@ -16,15 +16,15 @@ func CurveX25519() stdecdh.Curve { return x25519Curve } func NewX25519() KeyExchange { return x25519Impl } // GenerateKeyX25519 creates a new private key using crypto/rand. -func GenerateKeyX25519() (*stdecdh.PrivateKey, error) { return x25519Impl.GenerateKey() } +func GenerateKeyX25519() (PrivateKey, error) { return x25519Impl.GenerateKey() } // NewPrivateKeyX25519 constructs a private key from scalar bytes. -func NewPrivateKeyX25519(d []byte) (*stdecdh.PrivateKey, error) { return x25519Impl.NewPrivateKey(d) } +func NewPrivateKeyX25519(d []byte) (PrivateKey, error) { return x25519Impl.NewPrivateKey(d) } // NewPublicKeyX25519 parses a 32-byte Montgomery u-coordinate public key. -func NewPublicKeyX25519(b []byte) (*stdecdh.PublicKey, error) { return x25519Impl.NewPublicKey(b) } +func NewPublicKeyX25519(b []byte) (PublicKey, error) { return x25519Impl.NewPublicKey(b) } // SharedSecretX25519 performs the X25519 Diffie-Hellman operation between private and peer. -func SharedSecretX25519(p *stdecdh.PrivateKey, peer *stdecdh.PublicKey) ([]byte, error) { +func SharedSecretX25519(p PrivateKey, peer PublicKey) ([]byte, error) { return x25519Impl.SharedSecret(p, peer) } diff --git a/ecdh/x448.go b/ecdh/x448.go new file mode 100644 index 0000000..c62fb02 --- /dev/null +++ b/ecdh/x448.go @@ -0,0 +1,315 @@ +package ecdh + +import ( + "crypto" + stdecdh "crypto/ecdh" + "crypto/rand" + "crypto/subtle" + "errors" + "io" + "math/big" +) + +const ( + x448ScalarSize = 56 + x448PointSize = 56 + x448A24 = 39081 +) + +var ( + x448BasePoint = func() [x448PointSize]byte { + var bp [x448PointSize]byte + bp[0] = 5 + return bp + }() + + x448Prime = func() *big.Int { + two := big.NewInt(2) + p := new(big.Int).Exp(two, big.NewInt(448), nil) + tmp := new(big.Int).Exp(two, big.NewInt(224), nil) + p.Sub(p, tmp) + p.Sub(p, big.NewInt(1)) + return p + }() + + x448Impl = &x448KeyExchange{} + + errInvalidX448Scalar = errors.New("ecdh/x448: invalid private scalar length") + errInvalidX448Public = errors.New("ecdh/x448: invalid public key length") + errX448LowOrder = errors.New("ecdh/x448: low-order input point") +) + +// NewX448 returns a KeyExchange helper implementing the RFC 7748 X448 Diffie-Hellman +// primitive. The implementation is self-contained because crypto/ecdh currently +// does not expose Curve448. +func NewX448() KeyExchange { return x448Impl } + +// GenerateKeyX448 creates a new X448 private key using crypto/rand. +func GenerateKeyX448() (PrivateKey, error) { return x448Impl.GenerateKey() } + +// NewPrivateKeyX448 constructs an X448 private key from scalar bytes. +func NewPrivateKeyX448(d []byte) (PrivateKey, error) { return x448Impl.NewPrivateKey(d) } + +// NewPublicKeyX448 parses a 56-byte X448 public key. +func NewPublicKeyX448(b []byte) (PublicKey, error) { return x448Impl.NewPublicKey(b) } + +// SharedSecretX448 performs the X448 Diffie-Hellman operation between private and peer. +func SharedSecretX448(p PrivateKey, peer PublicKey) ([]byte, error) { + return x448Impl.SharedSecret(p, peer) +} + +type x448KeyExchange struct{} + +func (x *x448KeyExchange) Curve() stdecdh.Curve { return nil } + +func (x *x448KeyExchange) GenerateKey() (PrivateKey, error) { + var scalar [x448ScalarSize]byte + if _, err := io.ReadFull(rand.Reader, scalar[:]); err != nil { + return nil, err + } + clampScalarX448(scalar[:]) + priv := &x448PrivateKey{scalar: scalar} + return priv, nil +} + +func (x *x448KeyExchange) NewPrivateKey(d []byte) (PrivateKey, error) { + if len(d) != x448ScalarSize { + return nil, errInvalidX448Scalar + } + var scalar [x448ScalarSize]byte + copy(scalar[:], d) + clampScalarX448(scalar[:]) + return &x448PrivateKey{scalar: scalar}, nil +} + +func (x *x448KeyExchange) NewPublicKey(b []byte) (PublicKey, error) { + if len(b) != x448PointSize { + return nil, errInvalidX448Public + } + var pub [x448PointSize]byte + copy(pub[:], b) + return &x448PublicKey{u: pub}, nil +} + +func (x *x448KeyExchange) SharedSecret(p PrivateKey, peer PublicKey) ([]byte, error) { + priv, ok := p.(*x448PrivateKey) + if !ok { + return nil, errIncompatiblePrivate + } + pub, ok := peer.(*x448PublicKey) + if !ok { + return nil, errIncompatiblePublic + } + secret, err := priv.ECDH(pub) + if err != nil { + return nil, err + } + return secret, nil +} + +type x448PrivateKey struct { + scalar [x448ScalarSize]byte + public [x448PointSize]byte + computed bool +} + +func (k *x448PrivateKey) Bytes() []byte { + if k == nil { + return nil + } + out := make([]byte, x448ScalarSize) + copy(out, k.scalar[:]) + return out +} + +func (k *x448PrivateKey) PublicKey() PublicKey { + if k == nil { + return nil + } + k.ensurePublic() + return &x448PublicKey{u: k.public} +} + +func (k *x448PrivateKey) ECDH(peer PublicKey) ([]byte, error) { + if k == nil { + return nil, errors.New("ecdh/x448: nil private key") + } + other, ok := peer.(*x448PublicKey) + if !ok { + return nil, errIncompatiblePublic + } + var scalarCopy [x448ScalarSize]byte + copy(scalarCopy[:], k.scalar[:]) + var result [x448PointSize]byte + scalarMultX448(&result, &scalarCopy, &other.u) + if constantTimeAllZero(result[:]) == 1 { + return nil, errX448LowOrder + } + secret := make([]byte, x448PointSize) + copy(secret, result[:]) + return secret, nil +} + +func (k *x448PrivateKey) Equal(x crypto.PrivateKey) bool { + other, ok := x.(*x448PrivateKey) + if !ok { + return false + } + return subtle.ConstantTimeCompare(k.scalar[:], other.scalar[:]) == 1 +} + +func (k *x448PrivateKey) ensurePublic() { + if k.computed { + return + } + var result [x448PointSize]byte + var scalarCopy [x448ScalarSize]byte + copy(scalarCopy[:], k.scalar[:]) + scalarMultX448(&result, &scalarCopy, &x448BasePoint) + copy(k.public[:], result[:]) + k.computed = true +} + +type x448PublicKey struct { + u [x448PointSize]byte +} + +func (k *x448PublicKey) Bytes() []byte { + if k == nil { + return nil + } + out := make([]byte, x448PointSize) + copy(out, k.u[:]) + return out +} + +func (k *x448PublicKey) Equal(x crypto.PublicKey) bool { + other, ok := x.(*x448PublicKey) + if !ok { + return false + } + return subtle.ConstantTimeCompare(k.u[:], other.u[:]) == 1 +} + +func clampScalarX448(k []byte) { + k[0] &= 252 + k[55] |= 128 +} + +func scalarMultX448(out *[x448PointSize]byte, scalar *[x448ScalarSize]byte, point *[x448PointSize]byte) { + var k [x448ScalarSize]byte + copy(k[:], scalar[:]) + clampScalarX448(k[:]) + x1 := decodeLittleEndian(point[:]) + x2 := big.NewInt(1) + z2 := big.NewInt(0) + x3 := new(big.Int).Set(x1) + z3 := big.NewInt(1) + var swap uint64 + for t := 447; t >= 0; t-- { + bit := (uint64(k[t/8]) >> (uint(t) & 7)) & 1 + swap ^= bit + cswapBigInt(swap, x2, x3) + cswapBigInt(swap, z2, z3) + swap = bit + + a := modAdd(x2, z2) + b := modSub(x2, z2) + aa := modSquare(a) + bb := modSquare(b) + e := modSub(aa, bb) + c := modAdd(x3, z3) + d := modSub(x3, z3) + da := modMul(d, a) + cb := modMul(c, b) + x3 = modSquare(modAdd(da, cb)) + tmp := modSub(da, cb) + tmp = modSquare(tmp) + tmp = modMul(tmp, x1) + z3 = tmp + x2 = modMul(aa, bb) + z2 = modMul(e, modAdd(aa, modMulSmall(e, x448A24))) + } + cswapBigInt(swap, x2, x3) + cswapBigInt(swap, z2, z3) + + inv := modInverse(z2) + x2 = modMul(x2, inv) + encodeLittleEndian(out[:], x2) +} + +func cswapBigInt(swap uint64, x, y *big.Int) { + mask := new(big.Int).SetInt64(int64(-(int64(swap & 1)))) + tmp := new(big.Int).Xor(x, y) + tmp.And(tmp, mask) + x.Xor(x, tmp) + y.Xor(y, tmp) +} + +func modAdd(a, b *big.Int) *big.Int { + res := new(big.Int).Add(a, b) + res.Mod(res, x448Prime) + return res +} + +func modSub(a, b *big.Int) *big.Int { + res := new(big.Int).Sub(a, b) + res.Mod(res, x448Prime) + return res +} + +func modMul(a, b *big.Int) *big.Int { + res := new(big.Int).Mul(a, b) + res.Mod(res, x448Prime) + return res +} + +func modSquare(a *big.Int) *big.Int { + return modMul(a, a) +} + +func modMulSmall(a *big.Int, c int) *big.Int { + res := new(big.Int).Mul(a, big.NewInt(int64(c))) + res.Mod(res, x448Prime) + return res +} + +func modInverse(a *big.Int) *big.Int { + if a.Sign() == 0 { + return new(big.Int) + } + inv := new(big.Int).ModInverse(a, x448Prime) + if inv == nil { + return new(big.Int) + } + return inv +} + +func decodeLittleEndian(in []byte) *big.Int { + res := new(big.Int) + for i := len(in) - 1; i >= 0; i-- { + res.Lsh(res, 8) + res.Or(res, big.NewInt(int64(in[i]))) + } + res.Mod(res, x448Prime) + return res +} + +func encodeLittleEndian(out []byte, v *big.Int) { + value := new(big.Int).Mod(v, x448Prime) + bytes := value.Bytes() + for i := range out { + out[i] = 0 + } + for i := 0; i < len(bytes) && i < len(out); i++ { + out[i] = bytes[len(bytes)-1-i] + } +} + +func constantTimeAllZero(b []byte) int { + var acc byte + for _, v := range b { + acc |= v + } + return subtle.ConstantTimeByteEq(acc, 0) +} diff --git a/pq/hybrid.go b/pq/hybrid.go index 5bb43e9..436e293 100644 --- a/pq/hybrid.go +++ b/pq/hybrid.go @@ -1,7 +1,6 @@ package pq import ( - "crypto/rand" "encoding/binary" "errors" @@ -63,7 +62,7 @@ func (h *Hybrid) GenerateKey() (public, private []byte, err error) { if h == nil { return nil, nil, errors.New("pq: nil hybrid") } - classicalPriv, err := h.classical.Curve().GenerateKey(rand.Reader) + classicalPriv, err := h.classical.GenerateKey() if err != nil { return nil, nil, err } @@ -112,7 +111,7 @@ func (h *Hybrid) Encapsulate(public []byte) (ciphertext, sharedSecret []byte, er return nil, nil, errMissingPQPublic } - classicalPriv, err := h.classical.Curve().GenerateKey(rand.Reader) + classicalPriv, err := h.classical.GenerateKey() if err != nil { return nil, nil, err } @@ -191,7 +190,7 @@ func (h *Hybrid) Decapsulate(private, ciphertext []byte) ([]byte, error) { } } - classicalPriv, err := h.classical.Curve().NewPrivateKey(keyComponents.classical) + classicalPriv, err := h.classical.NewPrivateKey(keyComponents.classical) if err != nil { return nil, err } diff --git a/test/ecdh/benchmark_test.go b/test/ecdh/benchmark_test.go index 303c28b..6316cb5 100644 --- a/test/ecdh/benchmark_test.go +++ b/test/ecdh/benchmark_test.go @@ -12,6 +12,7 @@ func BenchmarkECDH(b *testing.B) { ke ecdh.KeyExchange }{ {"X25519", ecdh.NewX25519()}, + {"X448", ecdh.NewX448()}, {"P-256", ecdh.NewP256()}, {"P-384", ecdh.NewP384()}, } diff --git a/test/ecdh/testdata/x448_kat.json b/test/ecdh/testdata/x448_kat.json new file mode 100644 index 0000000..dfb324c --- /dev/null +++ b/test/ecdh/testdata/x448_kat.json @@ -0,0 +1,20 @@ +[ + { + "name": "RFC7748-1", + "scalar": "3D262FDDF9EC8E88495266FEA19A34D28882ACEF045104D0D1AAE121700A779C984C24F8CDD78FBFF44943EBA368F54B29259A4F1C600AD3", + "u": "06FCE640FA3487BFDA5F6CF2D5263F8AAD88334CBD07437F020F08F9814DC031DDBDC38C19C6DA2583FA5429DB94ADA18AA7A7FB4EF8A086", + "shared_secret": "CE3E4FF95A60DC6697DA1DB1D85E6AFBDF79B50A2412D7546D5F239FE14FBAADEB445FC66A01B0779D98223961111E21766282F73DD96B6F" + }, + { + "name": "RFC7748-2", + "scalar": "203D494428B8399352665DDCA42F9DE8FEF600908E0D461CB021F8C538345DD77C3E4806E25F46D3315C44E0A5B4371282DD2C8D5BE3095F", + "u": "0FBCC2F993CD56D3305B0B7D9E55D4C1A8FB5DBB52F8E9A1E9B6201B165D015894E56C4D3570BEE52FE205E28A78B91CDFBDE71CE8D157DB", + "shared_secret": "884A02576239FF7A2F2F63B2DB6A9FF37047AC13568E1E30FE63C4A7AD1B3EE3A5700DF34321D62077E63633C575C1C954514E99DA7C179D" + }, + { + "name": "RFC7748-DH", + "scalar": "9A8F4925D1519F5775CF46B04B5800D4EE9EE8BAE8BC5565D498C28DD9C9BAF574A9419744897391006382A6F127AB1D9AC2D8C0A598726B", + "u": "3EB7A829B0CD20F5BCFC0B599B6FECCF6DA4627107BDB0D4F345B43027D8B972FC3E34FB4232A13CA706DCB57AEC3DAE07BDC1C67BF33609", + "shared_secret": "07FFF4181AC6CC95EC1C16A94A0F74D12DA232CE40A77552281D282BB60C0B56FD2464C335543936521C24403085D59A449A5037514A879D" + } +] diff --git a/test/ecdh/x448_test.go b/test/ecdh/x448_test.go new file mode 100644 index 0000000..11e923f --- /dev/null +++ b/test/ecdh/x448_test.go @@ -0,0 +1,158 @@ +package ecdh_test + +import ( + "bytes" + "encoding/json" + "testing" + + xdh "github.com/AeonDave/cryptonite-go/ecdh" + "github.com/AeonDave/cryptonite-go/test/internal/testutil" + + _ "embed" +) + +//go:embed testdata/x448_kat.json +var x448KATJSON []byte + +type x448KATCase struct { + Name string `json:"name"` + Scalar string `json:"scalar"` + U string `json:"u"` + SharedSecret string `json:"shared_secret"` +} + +func loadX448KAT(t *testing.T) []x448KATCase { + t.Helper() + var cases []x448KATCase + if err := json.Unmarshal(x448KATJSON, &cases); err != nil { + t.Fatalf("failed to parse x448 KAT: %v", err) + } + if len(cases) == 0 { + t.Fatal("empty x448 KAT") + } + return cases +} + +func TestX448RFC7748Vectors(t *testing.T) { + cases := loadX448KAT(t) + + for i, tc := range cases { + priv, err := xdh.NewPrivateKeyX448(testutil.MustHex(t, tc.Scalar)) + if err != nil { + t.Fatalf("case %d (%s): NewPrivateKey failed: %v", i, tc.Name, err) + } + pub, err := xdh.NewPublicKeyX448(testutil.MustHex(t, tc.U)) + if err != nil { + t.Fatalf("case %d (%s): NewPublicKey failed: %v", i, tc.Name, err) + } + out, err := xdh.SharedSecretX448(priv, pub) + if err != nil { + t.Fatalf("case %d (%s): SharedSecret failed: %v", i, tc.Name, err) + } + if !bytes.Equal(out, testutil.MustHex(t, tc.SharedSecret)) { + t.Fatalf("case %d (%s): mismatch\n got %X\nwant %s", i, tc.Name, out, tc.SharedSecret) + } + } +} + +func TestX448IteratedVectors(t *testing.T) { + type iteration struct { + loops int + out string + } + + cases := []iteration{ + {loops: 1, out: "3F482C8A9F19B01E6C46EE9711D9DC14FD4BF67AF30765C2AE2B846A4D23A8CD0DB897086239492CAF350B51F833868B9BC2B3BCA9CF4113"}, + {loops: 1000, out: "AA3B4749D55B9DAF1E5B00288826C467274CE3EBBDD5C17B975E09D4AF6C67CF10D087202DB88286E2B79FCEEA3EC353EF54FAA26E219F38"}, + } + + seed := testutil.MustHex(t, "0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + + for _, tc := range cases { + k := append([]byte(nil), seed...) + u := append([]byte(nil), seed...) + for i := 0; i < tc.loops; i++ { + prevK := append([]byte(nil), k...) + priv, err := xdh.NewPrivateKeyX448(k) + if err != nil { + t.Fatalf("loops=%d: NewPrivateKey failed: %v", tc.loops, err) + } + pub, err := xdh.NewPublicKeyX448(u) + if err != nil { + t.Fatalf("loops=%d: NewPublicKey failed: %v", tc.loops, err) + } + shared, err := xdh.SharedSecretX448(priv, pub) + if err != nil { + t.Fatalf("loops=%d: SharedSecret failed: %v", tc.loops, err) + } + u = prevK + k = shared + } + if !bytes.Equal(k, testutil.MustHex(t, tc.out)) { + t.Fatalf("loops=%d: mismatch\n got %X\nwant %s", tc.loops, k, tc.out) + } + } +} + +func TestX448DiffieHellmanVector(t *testing.T) { + privA, err := xdh.NewPrivateKeyX448(testutil.MustHex(t, "9A8F4925D1519F5775CF46B04B5800D4EE9EE8BAE8BC5565D498C28DD9C9BAF574A9419744897391006382A6F127AB1D9AC2D8C0A598726B")) + if err != nil { + t.Fatalf("NewPrivateKey A failed: %v", err) + } + pubB, err := xdh.NewPublicKeyX448(testutil.MustHex(t, "3EB7A829B0CD20F5BCFC0B599B6FECCF6DA4627107BDB0D4F345B43027D8B972FC3E34FB4232A13CA706DCB57AEC3DAE07BDC1C67BF33609")) + if err != nil { + t.Fatalf("NewPublicKey B failed: %v", err) + } + secret, err := xdh.SharedSecretX448(privA, pubB) + if err != nil { + t.Fatalf("SharedSecret failed: %v", err) + } + if !bytes.Equal(secret, testutil.MustHex(t, "07FFF4181AC6CC95EC1C16A94A0F74D12DA232CE40A77552281D282BB60C0B56FD2464C335543936521C24403085D59A449A5037514A879D")) { + t.Fatalf("shared secret mismatch") + } +} + +func TestX448GenerateKey(t *testing.T) { + privA, err := xdh.GenerateKeyX448() + if err != nil { + t.Fatalf("GenerateKey A failed: %v", err) + } + privB, err := xdh.GenerateKeyX448() + if err != nil { + t.Fatalf("GenerateKey B failed: %v", err) + } + secretA, err := xdh.SharedSecretX448(privA, privB.PublicKey()) + if err != nil { + t.Fatalf("SharedSecret A failed: %v", err) + } + secretB, err := xdh.SharedSecretX448(privB, privA.PublicKey()) + if err != nil { + t.Fatalf("SharedSecret B failed: %v", err) + } + if !bytes.Equal(secretA, secretB) { + t.Fatalf("generated shared secrets differ") + } +} + +func TestX448Interface(t *testing.T) { + ke := xdh.NewX448() + priv, err := ke.GenerateKey() + if err != nil { + t.Fatalf("GenerateKey via interface failed: %v", err) + } + peer, err := ke.GenerateKey() + if err != nil { + t.Fatalf("peer GenerateKey failed: %v", err) + } + secretA, err := ke.SharedSecret(priv, peer.PublicKey()) + if err != nil { + t.Fatalf("SharedSecret failed: %v", err) + } + secretB, err := ke.SharedSecret(peer, priv.PublicKey()) + if err != nil { + t.Fatalf("SharedSecret peer failed: %v", err) + } + if !bytes.Equal(secretA, secretB) { + t.Fatalf("shared secrets via interface mismatch") + } +} diff --git a/test/pq/hybrid_test.go b/test/pq/hybrid_test.go index 2988555..501b373 100644 --- a/test/pq/hybrid_test.go +++ b/test/pq/hybrid_test.go @@ -218,7 +218,7 @@ func TestHybridWithPostQuantumStub(t *testing.T) { if !bytes.Equal(pqCT, stub.ciphertext) { t.Fatalf("unexpected PQ ciphertext component: %x", pqCT) } - privKey, err := base.Curve().NewPrivateKey(classicalPriv) + privKey, err := base.NewPrivateKey(classicalPriv) if err != nil { t.Fatalf("NewPrivateKey: %v", err) }