406 lines
14 KiB
Nim
406 lines
14 KiB
Nim
## tests/test_hash_verifier.nim
|
|
## Comprehensive unit tests for hash verification module
|
|
##
|
|
## Includes golden test vectors and edge case testing
|
|
|
|
import std/[unittest, os, streams, strutils, tempfiles, options]
|
|
import ../src/nimpak/security/hash_verifier
|
|
|
|
suite "Hash Algorithm Detection":
|
|
test "detect BLAKE2b hash formats":
|
|
check detectHashAlgorithm("blake2b-abc123") == HashBlake2b
|
|
check detectHashAlgorithm("a".repeat(128)) == HashBlake2b # 128 hex chars = BLAKE2b-512
|
|
|
|
test "detect BLAKE3 hash formats":
|
|
# BLAKE3 is detected but falls back to BLAKE2b
|
|
check detectHashAlgorithm("blake3-abc123") == HashBlake3
|
|
|
|
test "detect SHA256 hash formats":
|
|
check detectHashAlgorithm("sha256-abc123") == HashSha256
|
|
check detectHashAlgorithm("a".repeat(64)) == HashSha256 # 64 hex chars = SHA256
|
|
|
|
test "invalid hash formats":
|
|
expect ValueError:
|
|
discard detectHashAlgorithm("invalid-hash")
|
|
expect ValueError:
|
|
discard detectHashAlgorithm("abc") # Too short
|
|
|
|
suite "Hash String Parsing":
|
|
test "parse BLAKE2b hash strings":
|
|
let (alg1, digest1) = parseHashString("blake2b-abc123def456")
|
|
check alg1 == HashBlake2b
|
|
check digest1 == "abc123def456"
|
|
|
|
# Test with a valid 128-character BLAKE2b hash (without prefix)
|
|
let validBlake2bHash = "a".repeat(128)
|
|
let (alg2, digest2) = parseHashString(validBlake2bHash)
|
|
check alg2 == HashBlake2b
|
|
check digest2 == validBlake2bHash
|
|
|
|
test "format hash strings":
|
|
check formatHashString(HashBlake2b, "abc123") == "blake2b-abc123"
|
|
check formatHashString(HashBlake3, "def456") == "blake3-def456"
|
|
check formatHashString(HashSha256, "789abc") == "sha256-789abc"
|
|
|
|
suite "Golden Test Vectors":
|
|
# These are known-good test vectors for BLAKE2b-512
|
|
const BLAKE2B_GOLDEN_VECTORS = [
|
|
("", "blake2b-786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"),
|
|
("a", "blake2b-333fcb4ee1aa7c115355ec66ceac917c8bfd815bf7587d325aec1864edd24e34d5abe2c6b1b5ee3face62fed78dbef802f2a85cb91d455a8f5249d330853cb3c"),
|
|
("abc", "blake2b-ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923"),
|
|
("The quick brown fox jumps over the lazy dog", "blake2b-a8add4bdddfd93e4877d2746e62817b116364a1fa7bc148d95090bc7333b3673f82401cf7aa2e4cb1ecd90296e3f14cb5413f8ed77be73045b13914cdcd6a918"),
|
|
("NimPak", "blake2b-5274ed32fc344107b69805a6e77c9ab0dfc4c9b83d94a400bce3cc70d11f9d5c06738c319dace5b81859d56667550744b240f52d493dfc2e8afbbe76b00df1ae"),
|
|
("1234567890".repeat(10), "blake2b-800bb78cd4da18995c8074713bb6743cd94b2b6490a693fe4000ed00833b88b7b474d94af9cfed246b1b4ce1935a76154d7ea7c410493557741d18ec3a08da75")
|
|
]
|
|
|
|
# SHA256 test vectors for legacy support
|
|
const SHA256_GOLDEN_VECTORS = [
|
|
("", "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
|
|
("a", "sha256-ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"),
|
|
("abc", "sha256-ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
|
|
("The quick brown fox jumps over the lazy dog", "sha256-d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"),
|
|
("NimPak", "sha256-1ba47abd74914c637330282fad2a2eb807970548b532a552128d6de4af131cbf"),
|
|
("1234567890".repeat(10), "sha256-b20e12a7bcf7a0bcc5150265aab9c40b1d673781c143a73be76232d81e6038ec")
|
|
]
|
|
|
|
test "BLAKE2b golden vectors":
|
|
for (input, expectedHash) in BLAKE2B_GOLDEN_VECTORS:
|
|
let result = computeStringHash(input, HashBlake2b)
|
|
let formattedHash = formatHashString(result.algorithm, result.digest)
|
|
check formattedHash == expectedHash
|
|
check result.verified == false # Not verified yet
|
|
|
|
# Test verification
|
|
let verifyResult = verifyStringHash(input, expectedHash)
|
|
check verifyResult.verified == true
|
|
check verifyResult.digest == result.digest
|
|
|
|
test "SHA256 golden vectors":
|
|
for (input, expectedHash) in SHA256_GOLDEN_VECTORS:
|
|
let result = computeStringHash(input, HashSha256)
|
|
let formattedHash = formatHashString(result.algorithm, result.digest)
|
|
check formattedHash == expectedHash
|
|
check result.verified == false # Not verified yet
|
|
|
|
# Test verification
|
|
let verifyResult = verifyStringHash(input, expectedHash)
|
|
check verifyResult.verified == true
|
|
check verifyResult.digest == result.digest
|
|
|
|
suite "Streaming Hash Computation":
|
|
test "streaming hasher initialization":
|
|
let hasher = initStreamingHasher(HashBlake2b)
|
|
check hasher.algorithm == HashBlake2b
|
|
check hasher.bytesProcessed == 0
|
|
|
|
test "streaming hash with multiple updates":
|
|
var hasher = initStreamingHasher(HashBlake2b)
|
|
hasher.update("The quick ")
|
|
hasher.update("brown fox ")
|
|
hasher.update("jumps over ")
|
|
hasher.update("the lazy dog")
|
|
|
|
let result = hasher.finalize()
|
|
check hasher.bytesProcessed == 43 # Length of the string
|
|
|
|
# Should match the golden vector for the full string
|
|
let expectedResult = computeStringHash("The quick brown fox jumps over the lazy dog", HashBlake2b)
|
|
check result.digest == expectedResult.digest
|
|
|
|
test "streaming hash with byte arrays":
|
|
var hasher = initStreamingHasher(HashBlake2b)
|
|
let dataStr = "test data"
|
|
hasher.update(dataStr.toOpenArrayByte(0, dataStr.high))
|
|
|
|
let result = hasher.finalize()
|
|
check hasher.bytesProcessed == 9
|
|
|
|
# Should match direct string hash
|
|
let expectedResult = computeStringHash("test data", HashBlake2b)
|
|
check result.digest == expectedResult.digest
|
|
|
|
suite "File Hash Computation":
|
|
test "compute hash of existing file":
|
|
# Create a temporary file with known content
|
|
let (tempFile, tempPath) = createTempFile("nimpak_test_", ".txt")
|
|
tempFile.write("Hello, NimPak!")
|
|
tempFile.close()
|
|
|
|
try:
|
|
let result = computeFileHash(tempPath, HashBlake2b)
|
|
check result.algorithm == HashBlake2b
|
|
check result.digest.len == 128 # BLAKE2b-512 hex length
|
|
check result.computeTime >= 0.0
|
|
|
|
# Verify the hash
|
|
let expectedHash = formatHashString(result.algorithm, result.digest)
|
|
let verifyResult = verifyFileHash(tempPath, expectedHash)
|
|
check verifyResult.verified == true
|
|
|
|
finally:
|
|
removeFile(tempPath)
|
|
|
|
test "file not found error":
|
|
expect IOError:
|
|
discard computeFileHash("/nonexistent/file.txt")
|
|
|
|
test "large file handling":
|
|
# Create a larger temporary file (1MB)
|
|
let (tempFile, tempPath) = createTempFile("nimpak_large_", ".bin")
|
|
let chunkData = "A".repeat(1024) # 1KB chunk
|
|
|
|
for i in 0..<1024: # Write 1MB total
|
|
tempFile.write(chunkData)
|
|
tempFile.close()
|
|
|
|
try:
|
|
let result = computeFileHash(tempPath, HashBlake2b)
|
|
check result.algorithm == HashBlake2b
|
|
check result.digest.len == 128
|
|
check result.computeTime > 0.0
|
|
|
|
# Check that we can format the hash rate
|
|
let stats = getHashStatistics(result, 1024 * 1024)
|
|
check "MB/s" in stats or "KB/s" in stats or "GB/s" in stats
|
|
|
|
finally:
|
|
removeFile(tempPath)
|
|
|
|
suite "Stream Hash Computation":
|
|
test "compute hash from string stream":
|
|
let data = "Stream test data"
|
|
var stream = newStringStream(data)
|
|
|
|
let result = computeStreamHash(stream, HashBlake2b)
|
|
check result.algorithm == HashBlake2b
|
|
|
|
# Should match direct string hash
|
|
let expectedResult = computeStringHash(data, HashBlake2b)
|
|
check result.digest == expectedResult.digest
|
|
|
|
stream.close()
|
|
|
|
suite "Hash Verification":
|
|
test "successful hash verification":
|
|
let data = "Verification test"
|
|
let hashResult = computeStringHash(data, HashBlake2b)
|
|
let hashString = formatHashString(hashResult.algorithm, hashResult.digest)
|
|
|
|
let verifyResult = verifyStringHash(data, hashString)
|
|
check verifyResult.verified == true
|
|
check verifyResult.digest == hashResult.digest
|
|
|
|
test "failed hash verification":
|
|
let data = "Verification test"
|
|
let wrongHash = "blake2b-" & "0".repeat(128)
|
|
|
|
expect HashVerificationError:
|
|
discard verifyStringHash(data, wrongHash)
|
|
|
|
test "file hash verification":
|
|
let (tempFile, tempPath) = createTempFile("nimpak_verify_", ".txt")
|
|
tempFile.write("File verification test")
|
|
tempFile.close()
|
|
|
|
try:
|
|
# Compute correct hash
|
|
let hashResult = computeFileHash(tempPath, HashBlake2b)
|
|
let hashString = formatHashString(hashResult.algorithm, hashResult.digest)
|
|
|
|
# Verify should succeed
|
|
let verifyResult = verifyFileHash(tempPath, hashString)
|
|
check verifyResult.verified == true
|
|
|
|
# Wrong hash should fail
|
|
let wrongHash = "blake2b-" & "1".repeat(128)
|
|
expect HashVerificationError:
|
|
discard verifyFileHash(tempPath, wrongHash)
|
|
|
|
finally:
|
|
removeFile(tempPath)
|
|
|
|
suite "Batch Verification":
|
|
test "verify multiple files":
|
|
# Create multiple temporary files
|
|
var entries: seq[FileHashEntry] = @[]
|
|
var tempPaths: seq[string] = @[]
|
|
|
|
for i in 0..<3:
|
|
let (tempFile, tempPath) = createTempFile("nimpak_batch_", ".txt")
|
|
tempFile.write($"File content " & $i)
|
|
tempFile.close()
|
|
tempPaths.add(tempPath)
|
|
|
|
# Compute correct hash
|
|
let hashResult = computeFileHash(tempPath, HashBlake2b)
|
|
let hashString = formatHashString(hashResult.algorithm, hashResult.digest)
|
|
|
|
entries.add(FileHashEntry(
|
|
filePath: tempPath,
|
|
expectedHash: hashString,
|
|
result: none(HashResult),
|
|
error: ""
|
|
))
|
|
|
|
try:
|
|
let (verified, failed) = verifyMultipleFiles(entries)
|
|
check verified == 3
|
|
check failed == 0
|
|
|
|
for entry in entries:
|
|
check entry.result.isSome()
|
|
check entry.result.get().verified == true
|
|
check entry.error == ""
|
|
|
|
finally:
|
|
for path in tempPaths:
|
|
removeFile(path)
|
|
|
|
test "batch verification with failures":
|
|
var entries: seq[FileHashEntry] = @[
|
|
FileHashEntry(
|
|
filePath: "/nonexistent/file.txt",
|
|
expectedHash: "blake2b-" & "0".repeat(128),
|
|
result: none(HashResult),
|
|
error: ""
|
|
)
|
|
]
|
|
|
|
let (verified, failed) = verifyMultipleFiles(entries)
|
|
check verified == 0
|
|
check failed == 1
|
|
check entries[0].result.isNone()
|
|
check entries[0].error != ""
|
|
|
|
suite "Performance and Statistics":
|
|
test "hash rate formatting":
|
|
check formatHashRate(1000, 1.0) == "1.0 KB/s"
|
|
check formatHashRate(1_000_000, 1.0) == "1.0 MB/s"
|
|
check formatHashRate(1_000_000_000, 1.0) == "1.0 GB/s"
|
|
check formatHashRate(500, 1.0) == "500 B/s"
|
|
check formatHashRate(1000, 0.0) == "N/A"
|
|
|
|
test "hash statistics formatting":
|
|
let result = HashResult(
|
|
algorithm: HashBlake2b,
|
|
digest: "abc123",
|
|
verified: true,
|
|
computeTime: 0.5
|
|
)
|
|
|
|
let stats = getHashStatistics(result, 1000)
|
|
check "blake2b" in stats.toLower()
|
|
check "0.500s" in stats
|
|
check "KB/s" in stats
|
|
|
|
suite "Algorithm Fallback and Support":
|
|
test "algorithm fallback":
|
|
check getFallbackAlgorithm(HashBlake3) == HashBlake2b
|
|
check getFallbackAlgorithm(HashBlake2b) == HashBlake2b
|
|
check getFallbackAlgorithm(HashSha256) == HashSha256
|
|
|
|
test "algorithm support detection":
|
|
check isAlgorithmSupported(HashBlake2b) == true
|
|
check isAlgorithmSupported(HashSha256) == true
|
|
check isAlgorithmSupported(HashBlake3) == false # Not natively supported
|
|
|
|
test "BLAKE3 fallback behavior":
|
|
# BLAKE3 should fallback to BLAKE2b
|
|
let result = computeStringHash("test", HashBlake3)
|
|
check result.algorithm == HashBlake2b # Should have fallen back
|
|
|
|
# Compare with direct BLAKE2b computation
|
|
let blake2bResult = computeStringHash("test", HashBlake2b)
|
|
check result.digest == blake2bResult.digest
|
|
|
|
suite "Large File Handling":
|
|
test "large file hash computation":
|
|
# Create a file larger than the threshold for testing
|
|
let (tempFile, tempPath) = createTempFile("nimpak_large_", ".bin")
|
|
let chunkData = "X".repeat(1024) # 1KB chunk
|
|
|
|
# Write 2MB total (smaller than 1GB threshold for testing)
|
|
for i in 0..<2048:
|
|
tempFile.write(chunkData)
|
|
tempFile.close()
|
|
|
|
try:
|
|
var progressCalls = 0
|
|
let result = computeLargeFileHash(tempPath, HashBlake2b) do (processed: int64, total: int64):
|
|
inc progressCalls
|
|
check processed <= total
|
|
check processed >= 0
|
|
|
|
check result.algorithm == HashBlake2b
|
|
check result.digest.len == 128
|
|
check result.computeTime > 0.0
|
|
check progressCalls > 0 # Progress callback should have been called
|
|
|
|
# Verify the result matches regular file hash
|
|
let regularResult = computeFileHash(tempPath, HashBlake2b)
|
|
check result.digest == regularResult.digest
|
|
|
|
finally:
|
|
removeFile(tempPath)
|
|
|
|
test "chunk size optimization":
|
|
# Test that large files use larger chunk sizes
|
|
let (tempFile, tempPath) = createTempFile("nimpak_chunk_", ".bin")
|
|
let smallData = "small"
|
|
tempFile.write(smallData)
|
|
tempFile.close()
|
|
|
|
try:
|
|
let result = computeFileHash(tempPath, HashBlake2b)
|
|
check result.algorithm == HashBlake2b
|
|
check result.digest.len == 128
|
|
|
|
finally:
|
|
removeFile(tempPath)
|
|
|
|
suite "Utility Functions":
|
|
test "hash string validation":
|
|
check isValidHashString("blake2b-" & "a".repeat(128)) == true
|
|
check isValidHashString("sha256-" & "b".repeat(64)) == true
|
|
check isValidHashString("invalid-hash") == false
|
|
check isValidHashString("") == false
|
|
|
|
test "preferred algorithm":
|
|
check getPreferredHashAlgorithm() == HashBlake2b
|
|
|
|
test "supported algorithms":
|
|
let supported = getSupportedAlgorithms()
|
|
check HashBlake2b in supported
|
|
check HashSha256 in supported
|
|
# BLAKE3 not yet natively supported
|
|
check HashBlake3 notin supported
|
|
|
|
suite "Edge Cases and Error Handling":
|
|
test "empty string hash":
|
|
let result = computeStringHash("", HashBlake2b)
|
|
check result.digest.len == 128
|
|
check result.algorithm == HashBlake2b
|
|
|
|
test "very long string hash":
|
|
let longString = "A".repeat(100_000) # 100KB string
|
|
let result = computeStringHash(longString, HashBlake2b)
|
|
check result.digest.len == 128
|
|
check result.computeTime >= 0.0
|
|
|
|
test "unsupported algorithm error":
|
|
# BLAKE3 falls back to BLAKE2b, so no error
|
|
let hasher1 = initStreamingHasher(HashBlake3)
|
|
check hasher1.algorithm == HashBlake2b
|
|
|
|
# SHA256 is supported
|
|
let hasher2 = initStreamingHasher(HashSha256)
|
|
check hasher2.algorithm == HashSha256
|
|
|
|
test "hash verification error details":
|
|
try:
|
|
discard verifyStringHash("test", "blake2b-wrong")
|
|
fail()
|
|
except HashVerificationError as e:
|
|
check e.algorithm == HashBlake2b
|
|
check "wrong" in e.expectedHash
|
|
check e.actualHash != e.expectedHash |