Files
zeekay d070cd618e fix(security): unexport SignUnsafe + add VerifyConstantTime
Security improvements from audit:

1. SignUnsafe → signUnsafe (unexported)
   - No longer accessible outside primitives package
   - Prevents accidental double-signing in external code
   - Tests within package can still use it

2. VerifyConstantTime added
   - No early return on mismatch
   - Accumulates XOR differences across all 256 hashes
   - Prevents timing side-channel attacks
   - Use when verification timing is observable

Both Verify (fast) and VerifyConstantTime (secure) available.
2025-12-25 23:18:36 -08:00

132 lines
3.8 KiB
Go

package primitives
// Verify checks a Lamport signature against a public key and message.
//
// For each bit i of the message:
// - If bit i is 0, check keccak256(sig[i]) == pub[i][0]
// - If bit i is 1, check keccak256(sig[i]) == pub[i][1]
//
// Returns true if all 256 preimages hash to the correct public key values.
// NOTE: This function returns early on mismatch. For side-channel resistance,
// use VerifyConstantTime instead.
func Verify(pub *PublicKey, message [32]byte, sig *Signature) bool {
for i := 0; i < KeyBits; i++ {
bit := GetBit(message, i)
expectedHash := pub.Hashes[i][bit]
actualHash := Keccak256(sig.Preimages[i][:])
if actualHash != expectedHash {
return false
}
}
return true
}
// VerifyConstantTime checks a Lamport signature in constant time.
// Unlike Verify, this function always checks all 256 preimages regardless
// of mismatches, preventing timing side-channel attacks.
//
// Use this when the verification result could be observed by an attacker
// (e.g., through timing analysis).
func VerifyConstantTime(pub *PublicKey, message [32]byte, sig *Signature) bool {
var mismatch byte // Accumulate mismatches without branching
for i := 0; i < KeyBits; i++ {
bit := GetBit(message, i)
expectedHash := pub.Hashes[i][bit]
actualHash := Keccak256(sig.Preimages[i][:])
// XOR each byte and OR into mismatch accumulator
for j := 0; j < HashSize; j++ {
mismatch |= expectedHash[j] ^ actualHash[j]
}
}
// mismatch == 0 iff all hashes matched
return mismatch == 0
}
// VerifyBytes verifies a signature against message bytes.
func VerifyBytes(pub *PublicKey, message []byte, sig *Signature) bool {
if len(message) != 32 {
return false
}
var msg [32]byte
copy(msg[:], message)
return Verify(pub, msg, sig)
}
// VerifyU256 verifies a Lamport signature using uint256 bit representation.
// This matches the Solidity verify_u256 function exactly.
//
// Parameters:
// - bits: The 256-bit message as a big-endian byte array
// - sig: Array of 256 preimages
// - pub: 256x2 array of public key hashes
//
// Returns true if signature is valid.
func VerifyU256(bits [32]byte, sig [KeyBits][PreimageSize]byte, pub [KeyBits][2][HashSize]byte) bool {
for i := 0; i < KeyBits; i++ {
// Select pub[i][0] if bit is 0, pub[i][1] if bit is 1
// Bit ordering: bit 0 is MSB (position 255-i in Solidity's (1 << (255 - i)))
bit := GetBit(bits, i)
actualHash := Keccak256(sig[i][:])
if actualHash != pub[i][bit] {
return false
}
}
return true
}
// VerifyWithPKH verifies a signature and checks that the public key hashes to expectedPKH.
// This is useful for on-chain verification where only the PKH is stored.
func VerifyWithPKH(pub *PublicKey, message [32]byte, sig *Signature, expectedPKH [32]byte) bool {
// First check PKH matches
actualPKH := pub.Hash()
if actualPKH != expectedPKH {
return false
}
// Then verify signature
return Verify(pub, message, sig)
}
// VerifyThresholdMessage verifies a threshold Lamport signature with domain separation.
func VerifyThresholdMessage(
pub *PublicKey,
sig *Signature,
safeTxHash [32]byte,
nextPKH [32]byte,
moduleAddress [20]byte,
chainID uint64,
expectedPKH [32]byte,
) bool {
// Check PKH
if pub.Hash() != expectedPKH {
return false
}
// Compute domain-separated message
message := ComputeThresholdMessage(safeTxHash, nextPKH, moduleAddress, chainID)
// Verify signature
return Verify(pub, message, sig)
}
// BatchVerify verifies multiple signatures in parallel.
// Returns a slice of booleans indicating which signatures are valid.
func BatchVerify(pubs []*PublicKey, messages [][32]byte, sigs []*Signature) []bool {
n := len(pubs)
if len(messages) != n || len(sigs) != n {
results := make([]bool, n)
return results // All false
}
results := make([]bool, n)
for i := 0; i < n; i++ {
results[i] = Verify(pubs[i], messages[i], sigs[i])
}
return results
}