Skip to content

Commit

Permalink
Merge pull request #1 from anupsv/disperser-auth
Browse files Browse the repository at this point in the history
Fuzz tests for kms.go
  • Loading branch information
cody-littley authored Dec 18, 2024
2 parents d709520 + 260bf79 commit dbd0d24
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ dataapi-build:
unit-tests:
./test.sh

fuzz-tests:
go test --fuzz=FuzzParseSignatureKMS -fuzztime=5m ./common

integration-tests-churner:
go test -v ./churner/tests

Expand Down
9 changes: 9 additions & 0 deletions common/kms.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ func ParsePublicKeyKMS(bytes []byte) (*ecdsa.PublicKey, error) {
}

func adjustSignatureLength(buffer []byte) []byte {

if len(buffer) > 32 {
buffer = buffer[len(buffer)-32:] // Take last 32 bytes
}

buffer = bytes.TrimLeft(buffer, "\x00")
for len(buffer) < 32 {
zeroBuf := []byte{0}
Expand Down Expand Up @@ -117,6 +122,10 @@ func ParseSignatureKMS(
hash []byte,
bytes []byte) ([]byte, error) {

if !secp256k1.S256().IsOnCurve(publicKey.X, publicKey.Y) {
return nil, errors.New("public key is not on curve")
}

publicKeyBytes := secp256k1.S256().Marshal(publicKey.X, publicKey.Y)

var sigAsn1 asn1EcSig
Expand Down
245 changes: 245 additions & 0 deletions common/kms_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package common

import (
"bytes"
"crypto/ecdsa"
"crypto/sha256"
"encoding/asn1"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/crypto"
)

// ecdsaSignature defines the ASN.1 structure for ECDSA signatures.
type ecdsaSignature struct {
R, S *big.Int
}

// generateValidSignature generates a valid ECDSA signature and returns the public key, hash, and DER signature.
func generateValidSignature() (*ecdsa.PublicKey, []byte, []byte, error) {
// Generate a secp256k1 ECDSA key pair.
privateKey, err := crypto.GenerateKey()
if err != nil {
return nil, nil, nil, err
}
publicKey := &privateKey.PublicKey

// Define a message and compute its SHA-256 hash.
message := "Test message for ECDSA signature"
hash := sha256.Sum256([]byte(message))

// Sign the hash using the private key.
signatureBytes, err := crypto.Sign(hash[:], privateKey)
if err != nil {
return nil, nil, nil, err
}

// Convert the signature to DER format.
r := new(big.Int).SetBytes(signatureBytes[:32])
s := new(big.Int).SetBytes(signatureBytes[32:64])

// Marshal R and S into ASN.1 DER format.
derSignature, err := asn1.Marshal(ecdsaSignature{R: r, S: s})
if err != nil {
return nil, nil, nil, err
}

return publicKey, hash[:], derSignature, nil
}

// defineEdgeCases returns a slice of tuples containing publicKeyBytes, hashBytes, derSignatureBytes
func defineEdgeCases() [][3][]byte {
var edgeCases [][3][]byte

// Helper: Generate a valid signature to obtain a public key.
pubKeyValidBytes, hashValid, derSigValid, err := generateValidSignature()
if err != nil {
panic("Failed to generate valid signature for edge cases")
}
publicKeyValid := crypto.FromECDSAPub(pubKeyValidBytes)

// 1. Malformed Public Keys

// a. Incorrect length (too short)
publicKeyShort := []byte{0x04, 0x01, 0x02}
derSignatureValid := derSigValid
edgeCases = append(edgeCases, [3][]byte{publicKeyShort, hashValid, derSignatureValid})

// b. Incorrect prefix
publicKeyBadPrefix := make([]byte, 65)
publicKeyBadPrefix[0] = 0x05 // Invalid prefix
copy(publicKeyBadPrefix[1:], bytes.Repeat([]byte{0x01}, 64))
edgeCases = append(edgeCases, [3][]byte{publicKeyBadPrefix, hashValid, derSignatureValid})

// c. Coordinates not on curve (invalid X, Y)
publicKeyInvalidXY := make([]byte, 65)
publicKeyInvalidXY[0] = 0x04
// Set X and Y to values that are not on the curve
copy(publicKeyInvalidXY[1:], bytes.Repeat([]byte{0xFF}, 64))
edgeCases = append(edgeCases, [3][]byte{publicKeyInvalidXY, hashValid, derSignatureValid})

// 2. Malformed Signatures

// a. Invalid DER encoding (truncated)
derSignatureInvalidDER := []byte{0x30, 0x00} // Incomplete DER
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureInvalidDER})

// b. R too long (33 bytes with leading zero)
derSignatureRTooLong := []byte{
0x30, 0x46, // SEQUENCE, length 70
0x02, 0x21, // INTEGER, length 33
0x00, // Leading zero
}
derSignatureRTooLong = append(derSignatureRTooLong, bytes.Repeat([]byte{0x01}, 32)...) // R
derSignatureRTooLong = append(derSignatureRTooLong, 0x02, 0x20) // S INTEGER, length 32
derSignatureRTooLong = append(derSignatureRTooLong, bytes.Repeat([]byte{0x02}, 32)...) // S
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureRTooLong})

// c. S too short (31 bytes)
derSignatureSTooShort := []byte{
0x30, 0x44, // SEQUENCE, length 68
0x02, 0x20, // INTEGER, length 32
}
derSignatureSTooShort = append(derSignatureSTooShort, bytes.Repeat([]byte{0x03}, 32)...) // R
derSignatureSTooShort = append(derSignatureSTooShort, 0x02, 0x1F) // S INTEGER, length 31
derSignatureSTooShort = append(derSignatureSTooShort, bytes.Repeat([]byte{0x04}, 31)...) // S
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureSTooShort})

// 3. Invalid Hashes

// a. Incorrect hash length (too short)
hashTooShort := make([]byte, 16)
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashTooShort, derSignatureValid})

// b. Empty hash
hashEmpty := []byte{}
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashEmpty, derSignatureValid})

// 4. Random Data

// a. Completely random bytes
randomPublicKey := bytes.Repeat([]byte{0xAB}, 65)
randomHash := bytes.Repeat([]byte{0xCD}, 32)
randomSignature := bytes.Repeat([]byte{0xEF}, 70)
edgeCases = append(edgeCases, [3][]byte{randomPublicKey, randomHash, randomSignature})

// 5. Boundary Conditions

// a. R equals zero
derSignatureRZero, _ := asn1.Marshal(ecdsaSignature{R: big.NewInt(0), S: big.NewInt(1)})
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureRZero})

// b. S equals N (curve order)
secp256k1N := crypto.S256().Params().N
derSignatureSEqualsN, _ := asn1.Marshal(ecdsaSignature{R: big.NewInt(1), S: new(big.Int).Set(secp256k1N)})
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureSEqualsN})

// c. S just above N/2
secp256k1HalfN := new(big.Int).Div(crypto.S256().Params().N, big.NewInt(2))
sAboveHalfN := new(big.Int).Add(secp256k1HalfN, big.NewInt(1))
derSignatureSAboveHalfN, _ := asn1.Marshal(ecdsaSignature{R: big.NewInt(1), S: sAboveHalfN})
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureSAboveHalfN})

// d. S just below N/2
sBelowHalfN := new(big.Int).Sub(secp256k1HalfN, big.NewInt(1))
derSignatureSBelowHalfN, _ := asn1.Marshal(ecdsaSignature{R: big.NewInt(1), S: sBelowHalfN})
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureSBelowHalfN})

// 6. Extra Data

// a. Extra bytes appended to the signature
derSignatureExtra := append(derSignatureValid, 0x00, 0x01, 0x02)
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureExtra})

// b. Missing bytes in the signature
if len(derSignatureValid) > 2 {
derSignatureMissing := derSignatureValid[:len(derSignatureValid)-2]
edgeCases = append(edgeCases, [3][]byte{publicKeyValid, hashValid, derSignatureMissing})
}

return edgeCases
}

// FuzzParseSignatureKMS tests the ParseSignatureKMS function with various inputs, including edge cases.
func FuzzParseSignatureKMS(f *testing.F) {
// Generate multiple valid seed inputs
for i := 0; i < 5; i++ {
publicKey, hash, derSignature, err := generateValidSignature()
if err != nil {
f.Fatalf("Failed to generate valid signature: %v", err)
}
publicKeyBytes := crypto.FromECDSAPub(publicKey)
f.Add(publicKeyBytes, hash, derSignature)
}

// Incorporate edge cases into the fuzz corpus
edgeCases := defineEdgeCases()
for _, ec := range edgeCases {
f.Add(ec[0], ec[1], ec[2])
}

// Define the fuzzing function
f.Fuzz(func(t *testing.T, publicKeyBytes []byte, hashBytes []byte, derSignatureBytes []byte) {
// Skip iteration if publicKeyBytes is not the correct length
if len(publicKeyBytes) != 65 {
return
}

// Attempt to parse the public key
pubKey, err := ParsePublicKeyKMS(publicKeyBytes)
if err != nil {
// Invalid public key; acceptable for fuzzing
return
}

// Attempt to parse the signature
signature, err := ParseSignatureKMS(pubKey, hashBytes, derSignatureBytes)
if err != nil {
// Parsing failed; acceptable for fuzzing
return
}

// Validate that the signature is exactly 65 bytes
if len(signature) != 65 {
t.Errorf("Expected signature length 65 bytes, got %d bytes", len(signature))
}

// if the code made it this far, then the pubkey and signature are valid so recovery must work.
recoveredPubBytes, err := crypto.Ecrecover(hashBytes, signature)
if err != nil {
t.Errorf("Ecrecover failed: %v", err)
return
}

// Compare the recovered public key with the original
if !bytes.Equal(recoveredPubBytes, publicKeyBytes) {
// Attempt with the possible V values
signatureCheck := false
if signature[64] == 27 {
recoveredPubBytes, err = crypto.Ecrecover(hashBytes, signature)
if err != nil {
t.Errorf("Ecrecover failed with V=27: %v", err)
} else if !bytes.Equal(recoveredPubBytes, publicKeyBytes) {
t.Errorf("Recovered public key does not match original")
} else {
signatureCheck = true
}
}

if !signatureCheck {
signature[64] = 28
recoveredPubBytes, err = crypto.Ecrecover(hashBytes, signature)
if err != nil {
t.Errorf("Ecrecover failed with V=28: %v", err)
return
}

if !bytes.Equal(recoveredPubBytes, publicKeyBytes) {
t.Errorf("Recovered public key does not match original")
return
}
}
}
})
}

0 comments on commit dbd0d24

Please sign in to comment.