mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
367 lines
10 KiB
Go
367 lines
10 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package ring
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
|
|
"github.com/luxfi/crypto/mldsa"
|
|
)
|
|
|
|
// Post-quantum ring signature using ML-DSA (FIPS 204) key material.
|
|
//
|
|
// SECURITY NOTE: HASH-BASED RING SIGNATURE CONSTRUCTION
|
|
//
|
|
// This is a hash-based ring signature scheme using ML-DSA (FIPS 204) key material.
|
|
// The ring signature anonymity relies on SHA-512 properties, not lattice hardness.
|
|
// The "lattice" in the package name refers to the post-quantum key material only.
|
|
//
|
|
// The current construction provides:
|
|
// ✓ Post-quantum secure key material (ML-DSA-65, NIST Level 3)
|
|
// ✓ Ring signature anonymity (signer hidden among ring members)
|
|
// ✓ Linkability (same key produces same key image)
|
|
// ✓ Double-spend detection via key images
|
|
//
|
|
// CRITICAL LIMITATIONS:
|
|
// ✗ Security relies on hash function (SHA-512) properties, NOT lattice hardness
|
|
// ✗ Ring signature itself is NOT post-quantum secure against quantum adversaries
|
|
// ✗ Suitable ONLY for applications where PQ key material is required but classical
|
|
// ring signature security is acceptable (e.g., development, testing)
|
|
//
|
|
// For production post-quantum ring signatures, integrate one of:
|
|
// - Raptor (lattice-based ring signatures from NTRU)
|
|
// - MatRiCT (module-LWE based)
|
|
// - Calamari/Squid (isogeny-based)
|
|
// - Or implement from github.com/luxfi/lattice with proper ring signature construction
|
|
|
|
// Size constants based on ML-DSA-65 (192-bit security, NIST Level 3)
|
|
const (
|
|
// mldsaMode is the ML-DSA security level used
|
|
mldsaMode = mldsa.MLDSA65
|
|
|
|
// latticeKeyImageSize is the size of the key image (SHA-256 hash)
|
|
latticeKeyImageSize = 32
|
|
|
|
// latticeResponseSize is the size of each response value
|
|
latticeResponseSize = 64
|
|
)
|
|
|
|
// LatticeSignature implements ring signatures with post-quantum key material.
|
|
// IMPORTANT: This uses ML-DSA key material but the ring signature construction
|
|
// itself is hash-based (NOT a true lattice ring signature scheme).
|
|
// The construction provides anonymity among ring members with linkability.
|
|
type LatticeSignature struct {
|
|
keyImage []byte // Key image for linkability (32 bytes)
|
|
s [][]byte // Response values for each ring member (64 bytes each)
|
|
tag []byte // Signature tag binding all responses (64 bytes)
|
|
}
|
|
|
|
// Scheme returns LatticeLSAG.
|
|
func (sig *LatticeSignature) Scheme() Scheme {
|
|
return LatticeLSAG
|
|
}
|
|
|
|
// Bytes serializes the signature.
|
|
func (sig *LatticeSignature) Bytes() []byte {
|
|
n := len(sig.s)
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Format: keyImage (32) + tag (64) + n (4) + s values (n * 64)
|
|
size := latticeKeyImageSize + 64 + 4 + n*latticeResponseSize
|
|
data := make([]byte, size)
|
|
|
|
offset := 0
|
|
copy(data[offset:offset+latticeKeyImageSize], sig.keyImage)
|
|
offset += latticeKeyImageSize
|
|
|
|
copy(data[offset:offset+64], sig.tag)
|
|
offset += 64
|
|
|
|
binary.BigEndian.PutUint32(data[offset:offset+4], uint32(n))
|
|
offset += 4
|
|
|
|
for _, si := range sig.s {
|
|
copy(data[offset:offset+latticeResponseSize], si)
|
|
offset += latticeResponseSize
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// KeyImage returns the key image for linkability.
|
|
func (sig *LatticeSignature) KeyImage() []byte {
|
|
result := make([]byte, len(sig.keyImage))
|
|
copy(result, sig.keyImage)
|
|
return result
|
|
}
|
|
|
|
// RingSize returns the number of public keys in the ring.
|
|
func (sig *LatticeSignature) RingSize() int {
|
|
return len(sig.s)
|
|
}
|
|
|
|
// Verify verifies the lattice ring signature.
|
|
func (sig *LatticeSignature) Verify(message []byte, ring [][]byte) bool {
|
|
n := len(ring)
|
|
if n != len(sig.s) {
|
|
return false
|
|
}
|
|
if n < 2 {
|
|
return false
|
|
}
|
|
|
|
// Verify all public keys are valid ML-DSA keys
|
|
for _, pkBytes := range ring {
|
|
if _, err := mldsa.PublicKeyFromBytes(pkBytes, mldsaMode); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Recompute the signature tag
|
|
// tag = H(message, keyImage, H(s[0], P[0]), H(s[1], P[1]), ..., H(s[n-1], P[n-1]))
|
|
h := sha512.New()
|
|
h.Write([]byte("lattice-ring-verify"))
|
|
h.Write(message)
|
|
h.Write(sig.keyImage)
|
|
|
|
for i := 0; i < n; i++ {
|
|
// Compute commitment for position i
|
|
commitment := latticeCommitment(sig.s[i], ring[i], sig.keyImage, i)
|
|
h.Write(commitment)
|
|
}
|
|
|
|
expectedTag := h.Sum(nil)
|
|
|
|
return constantTimeCompare(sig.tag, expectedTag)
|
|
}
|
|
|
|
// ParseLatticeSignature parses a lattice ring signature from bytes.
|
|
func ParseLatticeSignature(data []byte) (*LatticeSignature, error) {
|
|
minSize := latticeKeyImageSize + 64 + 4
|
|
if len(data) < minSize {
|
|
return nil, errors.New("signature too short")
|
|
}
|
|
|
|
sig := &LatticeSignature{}
|
|
|
|
offset := 0
|
|
sig.keyImage = make([]byte, latticeKeyImageSize)
|
|
copy(sig.keyImage, data[offset:offset+latticeKeyImageSize])
|
|
offset += latticeKeyImageSize
|
|
|
|
sig.tag = make([]byte, 64)
|
|
copy(sig.tag, data[offset:offset+64])
|
|
offset += 64
|
|
|
|
n := int(binary.BigEndian.Uint32(data[offset : offset+4]))
|
|
offset += 4
|
|
|
|
if n < 2 || n > 1000 {
|
|
return nil, errors.New("invalid ring size")
|
|
}
|
|
|
|
expectedSize := minSize + n*latticeResponseSize
|
|
if len(data) != expectedSize {
|
|
return nil, errors.New("invalid signature length")
|
|
}
|
|
|
|
sig.s = make([][]byte, n)
|
|
for i := 0; i < n; i++ {
|
|
sig.s[i] = make([]byte, latticeResponseSize)
|
|
copy(sig.s[i], data[offset:offset+latticeResponseSize])
|
|
offset += latticeResponseSize
|
|
}
|
|
|
|
return sig, nil
|
|
}
|
|
|
|
// LatticeSigner creates ring signatures using ML-DSA (post-quantum) key material.
|
|
// Note: While the keys are post-quantum secure, the ring signature construction
|
|
// is hash-based and provides classical (not post-quantum) ring signature security.
|
|
type LatticeSigner struct {
|
|
privateKey *mldsa.PrivateKey
|
|
publicKey *mldsa.PublicKey
|
|
keyImage []byte
|
|
// secretScalar is derived from private key for ring signature math
|
|
secretScalar []byte
|
|
}
|
|
|
|
// NewLatticeSigner creates a new ring signer using ML-DSA key material.
|
|
func NewLatticeSigner(reader io.Reader) (*LatticeSigner, error) {
|
|
if reader == nil {
|
|
reader = rand.Reader
|
|
}
|
|
|
|
// Generate ML-DSA key pair
|
|
privKey, err := mldsa.GenerateKey(reader, mldsaMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newLatticeSignerFromKey(privKey)
|
|
}
|
|
|
|
// NewLatticeSignerFromPrivateKey creates a signer from an existing private key.
|
|
func NewLatticeSignerFromPrivateKey(privateKey []byte) (*LatticeSigner, error) {
|
|
privKey, err := mldsa.PrivateKeyFromBytes(mldsaMode, privateKey)
|
|
if err != nil {
|
|
return nil, ErrInvalidPrivateKey
|
|
}
|
|
|
|
return newLatticeSignerFromKey(privKey)
|
|
}
|
|
|
|
func newLatticeSignerFromKey(privKey *mldsa.PrivateKey) (*LatticeSigner, error) {
|
|
// Derive a secret scalar from the private key for ring signature operations
|
|
secretScalar := deriveSecretScalar(privKey.Bytes())
|
|
|
|
// Compute key image: I = x * H_p(P) (simulated with hashing)
|
|
pubKeyBytes := privKey.PublicKey.Bytes()
|
|
keyImage := computeLatticeKeyImage(secretScalar, pubKeyBytes)
|
|
|
|
return &LatticeSigner{
|
|
privateKey: privKey,
|
|
publicKey: privKey.PublicKey,
|
|
keyImage: keyImage,
|
|
secretScalar: secretScalar,
|
|
}, nil
|
|
}
|
|
|
|
// Scheme returns LatticeLSAG.
|
|
func (s *LatticeSigner) Scheme() Scheme {
|
|
return LatticeLSAG
|
|
}
|
|
|
|
// PublicKey returns the signer's public key.
|
|
func (s *LatticeSigner) PublicKey() []byte {
|
|
return s.publicKey.Bytes()
|
|
}
|
|
|
|
// KeyImage returns the key image.
|
|
func (s *LatticeSigner) KeyImage() []byte {
|
|
result := make([]byte, len(s.keyImage))
|
|
copy(result, s.keyImage)
|
|
return result
|
|
}
|
|
|
|
// Sign creates a ring signature using the hash-based construction with ML-DSA keys.
|
|
func (signer *LatticeSigner) Sign(message []byte, ring [][]byte, signerIndex int) (RingSignature, error) {
|
|
n := len(ring)
|
|
if n < 2 {
|
|
return nil, ErrInvalidRingSize
|
|
}
|
|
if signerIndex < 0 || signerIndex >= n {
|
|
return nil, ErrInvalidSignerIndex
|
|
}
|
|
|
|
// Verify our public key is at the specified index
|
|
if !constantTimeCompare(ring[signerIndex], signer.publicKey.Bytes()) {
|
|
return nil, errors.New("signer public key not at specified index")
|
|
}
|
|
|
|
// Verify all public keys are valid ML-DSA keys
|
|
for i, pkBytes := range ring {
|
|
if _, err := mldsa.PublicKeyFromBytes(pkBytes, mldsaMode); err != nil {
|
|
return nil, errors.New("invalid public key at index " + string(rune('0'+i)))
|
|
}
|
|
}
|
|
|
|
// Generate response values for all positions
|
|
s := make([][]byte, n)
|
|
|
|
for i := 0; i < n; i++ {
|
|
if i == signerIndex {
|
|
// For signer: generate deterministic response using secret
|
|
s[i] = latticeSignerResponse(message, ring, signer.secretScalar, signer.keyImage, signerIndex)
|
|
} else {
|
|
// For non-signers: generate random response
|
|
s[i] = make([]byte, latticeResponseSize)
|
|
if _, err := rand.Read(s[i]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute signature tag
|
|
h := sha512.New()
|
|
h.Write([]byte("lattice-ring-verify"))
|
|
h.Write(message)
|
|
h.Write(signer.keyImage)
|
|
|
|
for i := 0; i < n; i++ {
|
|
commitment := latticeCommitment(s[i], ring[i], signer.keyImage, i)
|
|
h.Write(commitment)
|
|
}
|
|
|
|
tag := h.Sum(nil)
|
|
|
|
return &LatticeSignature{
|
|
keyImage: signer.keyImage,
|
|
s: s,
|
|
tag: tag,
|
|
}, nil
|
|
}
|
|
|
|
// Helper functions for lattice ring signature operations
|
|
|
|
// deriveSecretScalar derives a deterministic secret scalar from the private key
|
|
func deriveSecretScalar(privateKey []byte) []byte {
|
|
h := sha512.New()
|
|
h.Write([]byte("lattice-ring-secret-scalar"))
|
|
h.Write(privateKey)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// computeLatticeKeyImage computes the key image from secret scalar and public key
|
|
func computeLatticeKeyImage(secretScalar, publicKey []byte) []byte {
|
|
// Key image: I = H(secret, H_p(P))
|
|
h := sha256.New()
|
|
h.Write([]byte("lattice-keyimage"))
|
|
|
|
// H_p(P)
|
|
hp := sha256.Sum256(publicKey)
|
|
|
|
// Combine with secret to create linkable key image
|
|
h.Write(hp[:])
|
|
h.Write(secretScalar)
|
|
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// latticeCommitment computes a commitment for position i in the ring
|
|
func latticeCommitment(s, publicKey, keyImage []byte, index int) []byte {
|
|
h := sha512.New()
|
|
h.Write([]byte("lattice-commitment"))
|
|
h.Write(s)
|
|
h.Write(publicKey)
|
|
h.Write(keyImage)
|
|
binary.Write(h, binary.BigEndian, uint32(index))
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// latticeSignerResponse generates the signer's response value
|
|
// This is deterministic based on the secret, message, and ring
|
|
func latticeSignerResponse(message []byte, ring [][]byte, secretScalar, keyImage []byte, signerIndex int) []byte {
|
|
h := sha512.New()
|
|
h.Write([]byte("lattice-signer-response"))
|
|
h.Write(message)
|
|
h.Write(secretScalar)
|
|
h.Write(keyImage)
|
|
binary.Write(h, binary.BigEndian, uint32(signerIndex))
|
|
|
|
// Include ring in the response derivation for binding
|
|
for _, pk := range ring {
|
|
h.Write(pk)
|
|
}
|
|
|
|
return h.Sum(nil)
|
|
}
|