From 95f70ddf21f7b845ab84d1d070e511da54a91df4 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Mon, 14 Apr 2025 08:33:12 -0400 Subject: [PATCH] lib: add Verifier interface Signed-off-by: Xe Iaso --- lib/verifier.go | 70 ++++++++++++++++++++++++++ lib/verifier_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 lib/verifier.go create mode 100644 lib/verifier_test.go diff --git a/lib/verifier.go b/lib/verifier.go new file mode 100644 index 0000000..07e1518 --- /dev/null +++ b/lib/verifier.go @@ -0,0 +1,70 @@ +package lib + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "errors" + "fmt" +) + +var ( + ErrChallengeFailed = errors.New("libanubis: challenge failed, hash does not match what the server calculated") + ErrWrongChallengeDifficulty = errors.New("libanubis: wrong challenge difficulty") +) + +type Verifier interface { + Verify(ctx context.Context, challenge, verify []byte, nonce, difficulty uint32) (bool, error) +} + +type VerifierFunc func(ctx context.Context, challenge, verify []byte, nonce, difficulty uint32) (bool, error) + +func (vf VerifierFunc) Verify(ctx context.Context, challenge, verify []byte, nonce, difficulty uint32) (bool, error) { + return vf(ctx, challenge, verify, nonce, difficulty) +} + +func BasicSHA256Verify(ctx context.Context, challenge, verify []byte, nonce, difficulty uint32) (bool, error) { + h := sha256.New() + fmt.Fprintf(h, "%x%d", challenge, nonce) + data := h.Sum(nil) + + if subtle.ConstantTimeCompare(data, verify) != 1 { + return false, fmt.Errorf("%w: wanted %x, got: %x", ErrChallengeFailed, verify, data) + } + + if !hasLeadingZeroNibbles(data, difficulty) { + return false, fmt.Errorf("%w: wanted %d leading zeroes in calculated data %x, but did not get it", ErrWrongChallengeDifficulty, data, difficulty) + } + + if !hasLeadingZeroNibbles(verify, difficulty) { + return false, fmt.Errorf("%w: wanted %d leading zeroes in verification data %x, but did not get it", ErrWrongChallengeDifficulty, verify, difficulty) + } + + return true, nil +} + +// hasLeadingZeroNibbles checks if the first `n` nibbles (in order) are zero. +// Nibbles are read from high to low for each byte (e.g., 0x12 -> nibbles [0x1, 0x2]). +func hasLeadingZeroNibbles(data []byte, n uint32) bool { + count := uint32(0) + for _, b := range data { + // Check high nibble (first 4 bits) + if (b >> 4) != 0 { + break // Non-zero found in leading nibbles + } + count++ + if count >= n { + return true + } + + // Check low nibble (last 4 bits) + if (b & 0x0F) != 0 { + break // Non-zero found in leading nibbles + } + count++ + if count >= n { + return true + } + } + return count >= n +} diff --git a/lib/verifier_test.go b/lib/verifier_test.go new file mode 100644 index 0000000..15719bb --- /dev/null +++ b/lib/verifier_test.go @@ -0,0 +1,114 @@ +package lib + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "testing" +) + +// echo -n "hi2" | sha256sum +const hi2SHA256 = "0251f1ec2880f67631b8d0b3a62cf71a17dfa31858a323e7fc38068fcfaeded0" +const nonce uint32 = 5 +const expectedVerifyString = "0543cbd94db5da055e82263cb775ac16f59fbbc1900645458baa197f9036ae9d" + +func TestBasicSHA256Verify(t *testing.T) { + ctx := context.Background() + + challenge, err := hex.DecodeString(hi2SHA256) + if err != nil { + t.Fatalf("[unexpected] %s does not decode as hex", hi2SHA256) + } + + expectedVerify, err := hex.DecodeString(expectedVerifyString) + if err != nil { + t.Fatalf("[unexpected] %s does not decode as hex", expectedVerifyString) + } + + t.Logf("got nonce: %d", nonce) + t.Logf("got hash: %x", expectedVerify) + + invalidVerify := make([]byte, len(expectedVerify)) + copy(invalidVerify, expectedVerify) + invalidVerify[len(invalidVerify)-1] ^= 0xFF // Flip the last byte + + testCases := []struct { + name string + challenge []byte + verify []byte + nonce uint32 + difficulty uint32 + want bool + expectError error + }{ + { + name: "valid verification", + challenge: challenge, + verify: expectedVerify, + nonce: nonce, + difficulty: 1, + want: true, + expectError: nil, + }, + { + name: "invalid verify data", + challenge: challenge, + verify: invalidVerify, + nonce: nonce, + difficulty: 1, + want: false, + expectError: ErrChallengeFailed, + }, + { + name: "insufficient computed data difficulty", + challenge: challenge, + verify: expectedVerify, + nonce: nonce, + difficulty: 5, + want: false, + expectError: ErrWrongChallengeDifficulty, + }, + { + name: "zero difficulty", + challenge: challenge, + verify: expectedVerify, + nonce: nonce, + difficulty: 0, + want: true, + expectError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := BasicSHA256Verify(ctx, tc.challenge, tc.verify, tc.nonce, tc.difficulty) + if !errors.Is(err, tc.expectError) { + t.Errorf("BasicSHA256Verify() error = %v, expectError %v", err, tc.expectError) + return + } + if got != tc.want { + t.Errorf("BasicSHA256Verify() got = %v, want %v", got, tc.want) + } + }) + } +} + +func TestHasLeadingZeroNibbles(t *testing.T) { + for _, cs := range []struct { + data []byte + difficulty uint32 + valid bool + }{ + {[]byte{0x10, 0x00}, 1, false}, + {[]byte{0x00, 0x00}, 4, true}, + {[]byte{0x01, 0x00}, 4, false}, + } { + t.Run(fmt.Sprintf("%x-%d-%v", cs.data, cs.difficulty, cs.valid), func(t *testing.T) { + result := hasLeadingZeroNibbles(cs.data, cs.difficulty) + if result != cs.valid { + t.Errorf("wanted %v, but got: %v", cs.valid, result) + } + }) + } +}