mirror of
https://github.com/caddyserver/caddy.git
synced 2026-06-07 07:14:27 -04:00
add caddyauth tests
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
This commit is contained in:
324
modules/caddyhttp/caddyauth/argon2id_test.go
Normal file
324
modules/caddyhttp/caddyauth/argon2id_test.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
func TestArgon2idHashCaddyModule(t *testing.T) {
|
||||
a := Argon2idHash{}
|
||||
info := a.CaddyModule()
|
||||
if info.ID != "http.authentication.hashes.argon2id" {
|
||||
t.Errorf("CaddyModule().ID = %v, want 'http.authentication.hashes.argon2id'", info.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idDecodeHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
wantErr bool
|
||||
wantErrStr string
|
||||
}{
|
||||
{
|
||||
name: "valid hash",
|
||||
hash: "$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg",
|
||||
},
|
||||
{
|
||||
name: "too few parts",
|
||||
hash: "$argon2id$v=19$m=47104,t=1,p=1",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong variant",
|
||||
hash: "$argon2i$v=19$m=47104,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
wantErrStr: "unsupported variant",
|
||||
},
|
||||
{
|
||||
name: "invalid version",
|
||||
hash: "$argon2id$v=abc$m=47104,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "incompatible version",
|
||||
hash: "$argon2id$v=18$m=47104,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid parameters - too few",
|
||||
hash: "$argon2id$v=19$m=47104,t=1$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid memory parameter",
|
||||
hash: "$argon2id$v=19$m=abc,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid iterations parameter",
|
||||
hash: "$argon2id$v=19$m=47104,t=abc,p=1$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid parallelism parameter",
|
||||
hash: "$argon2id$v=19$m=47104,t=1,p=abc$c29tZXNhbHQ$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid salt base64",
|
||||
hash: "$argon2id$v=19$m=47104,t=1,p=1$!!!invalid!!!$c29tZWtleQ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid key base64",
|
||||
hash: "$argon2id$v=19$m=47104,t=1,p=1$c29tZXNhbHQ$!!!invalid!!!",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
hash: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
argHash, key, err := DecodeHash([]byte(tt.hash))
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("DecodeHash() should return error")
|
||||
}
|
||||
if tt.wantErrStr != "" && !strings.Contains(err.Error(), tt.wantErrStr) {
|
||||
t.Errorf("error %q should contain %q", err.Error(), tt.wantErrStr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeHash() error: %v", err)
|
||||
}
|
||||
if argHash == nil {
|
||||
t.Fatal("DecodeHash() returned nil hash")
|
||||
}
|
||||
if key == nil {
|
||||
t.Fatal("DecodeHash() returned nil key")
|
||||
}
|
||||
if argHash.time == 0 {
|
||||
t.Error("decoded time is 0")
|
||||
}
|
||||
if argHash.memory == 0 {
|
||||
t.Error("decoded memory is 0")
|
||||
}
|
||||
if argHash.threads == 0 {
|
||||
t.Error("decoded threads is 0")
|
||||
}
|
||||
if len(argHash.salt) == 0 {
|
||||
t.Error("decoded salt is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idDecodeHashParsesCorrectValues(t *testing.T) {
|
||||
hash := "$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg"
|
||||
argHash, _, err := DecodeHash([]byte(hash))
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeHash() error: %v", err)
|
||||
}
|
||||
|
||||
if argHash.memory != 47104 {
|
||||
t.Errorf("memory = %d, want 47104", argHash.memory)
|
||||
}
|
||||
if argHash.time != 1 {
|
||||
t.Errorf("time = %d, want 1", argHash.time)
|
||||
}
|
||||
if argHash.threads != 1 {
|
||||
t.Errorf("threads = %d, want 1", argHash.threads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idCompare(t *testing.T) {
|
||||
hasher := Argon2idHash{
|
||||
time: defaultArgon2idTime,
|
||||
memory: defaultArgon2idMemory,
|
||||
threads: defaultArgon2idThreads,
|
||||
keyLen: defaultArgon2idKeylen,
|
||||
}
|
||||
|
||||
plaintext := []byte("test-password")
|
||||
hashed, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
plaintext []byte
|
||||
wantMatch bool
|
||||
}{
|
||||
{name: "correct password", plaintext: plaintext, wantMatch: true},
|
||||
{name: "wrong password", plaintext: []byte("wrong"), wantMatch: false},
|
||||
{name: "empty password", plaintext: []byte(""), wantMatch: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
match, err := hasher.Compare(hashed, tt.plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() error: %v", err)
|
||||
}
|
||||
if match != tt.wantMatch {
|
||||
t.Errorf("Compare() = %v, want %v", match, tt.wantMatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idHashRoundTrip(t *testing.T) {
|
||||
hasher := Argon2idHash{
|
||||
time: defaultArgon2idTime,
|
||||
memory: defaultArgon2idMemory,
|
||||
threads: defaultArgon2idThreads,
|
||||
keyLen: defaultArgon2idKeylen,
|
||||
}
|
||||
plaintext := []byte("round-trip-test")
|
||||
|
||||
hashed, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error: %v", err)
|
||||
}
|
||||
|
||||
// Verify hash format starts with $argon2id$
|
||||
if !strings.HasPrefix(string(hashed), "$argon2id$v=") {
|
||||
t.Errorf("Hash() output %q doesn't start with '$argon2id$v='", hashed)
|
||||
}
|
||||
|
||||
match, err := hasher.Compare(hashed, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() error: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Error("Hash then Compare round-trip failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idHashProducesDifferentHashes(t *testing.T) {
|
||||
hasher := Argon2idHash{
|
||||
time: defaultArgon2idTime,
|
||||
memory: defaultArgon2idMemory,
|
||||
threads: defaultArgon2idThreads,
|
||||
keyLen: defaultArgon2idKeylen,
|
||||
}
|
||||
plaintext := []byte("same-password")
|
||||
|
||||
hash1, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() first error: %v", err)
|
||||
}
|
||||
hash2, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() second error: %v", err)
|
||||
}
|
||||
|
||||
// Different salts should produce different hashes
|
||||
if string(hash1) == string(hash2) {
|
||||
t.Error("Hash() should produce different output on each call (different salt)")
|
||||
}
|
||||
|
||||
// But both should verify correctly
|
||||
match1, _ := hasher.Compare(hash1, plaintext)
|
||||
match2, _ := hasher.Compare(hash2, plaintext)
|
||||
if !match1 || !match2 {
|
||||
t.Error("Both hashes should verify against original plaintext")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idFakeHash(t *testing.T) {
|
||||
hasher := Argon2idHash{}
|
||||
fake := hasher.FakeHash()
|
||||
|
||||
if len(fake) == 0 {
|
||||
t.Fatal("FakeHash() returned empty result")
|
||||
}
|
||||
|
||||
// Should be a valid argon2id hash
|
||||
_, _, err := DecodeHash(fake)
|
||||
if err != nil {
|
||||
t.Errorf("FakeHash() is not a valid argon2id hash: %v", err)
|
||||
}
|
||||
|
||||
// Should match the known plaintext "antitiming"
|
||||
match, err := hasher.Compare(fake, []byte("antitiming"))
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() with FakeHash error: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Error("FakeHash() should match plaintext 'antitiming'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idCompareWithInvalidHash(t *testing.T) {
|
||||
hasher := Argon2idHash{}
|
||||
_, err := hasher.Compare([]byte("not-a-hash"), []byte("password"))
|
||||
if err == nil {
|
||||
t.Error("Compare() with invalid hash should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgon2idHashVersionInOutput(t *testing.T) {
|
||||
hasher := Argon2idHash{
|
||||
time: defaultArgon2idTime,
|
||||
memory: defaultArgon2idMemory,
|
||||
threads: defaultArgon2idThreads,
|
||||
keyLen: defaultArgon2idKeylen,
|
||||
}
|
||||
|
||||
hashed, err := hasher.Hash([]byte("test"))
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error: %v", err)
|
||||
}
|
||||
|
||||
// Verify version field matches current argon2 version
|
||||
versionStr := "v=" + strings.Split(string(hashed), "$")[2]
|
||||
if versionStr != "v=v=19" {
|
||||
// argon2.Version is 0x13 = 19
|
||||
expectedVersion := "$argon2id$v=19$"
|
||||
if !strings.Contains(string(hashed), expectedVersion[:len(expectedVersion)-1]) {
|
||||
t.Errorf("Hash output should contain version %d, got %q", argon2.Version, hashed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSalt(t *testing.T) {
|
||||
salt1, err := generateSalt(16)
|
||||
if err != nil {
|
||||
t.Fatalf("generateSalt(16) error: %v", err)
|
||||
}
|
||||
if len(salt1) != 16 {
|
||||
t.Errorf("generateSalt(16) length = %d, want 16", len(salt1))
|
||||
}
|
||||
|
||||
salt2, err := generateSalt(16)
|
||||
if err != nil {
|
||||
t.Fatalf("generateSalt(16) second call error: %v", err)
|
||||
}
|
||||
|
||||
// Two salts should be different (with overwhelming probability)
|
||||
if string(salt1) == string(salt2) {
|
||||
t.Error("generateSalt() should produce different values on each call")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSaltLengths(t *testing.T) {
|
||||
for _, length := range []int{8, 16, 32, 64} {
|
||||
salt, err := generateSalt(length)
|
||||
if err != nil {
|
||||
t.Fatalf("generateSalt(%d) error: %v", length, err)
|
||||
}
|
||||
if len(salt) != length {
|
||||
t.Errorf("generateSalt(%d) length = %d, want %d", length, len(salt), length)
|
||||
}
|
||||
}
|
||||
}
|
||||
155
modules/caddyhttp/caddyauth/bcrypt_test.go
Normal file
155
modules/caddyhttp/caddyauth/bcrypt_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestBcryptHashCaddyModule(t *testing.T) {
|
||||
b := BcryptHash{}
|
||||
info := b.CaddyModule()
|
||||
if info.ID != "http.authentication.hashes.bcrypt" {
|
||||
t.Errorf("CaddyModule().ID = %v, want 'http.authentication.hashes.bcrypt'", info.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHashCompare(t *testing.T) {
|
||||
hasher := BcryptHash{}
|
||||
plaintext := []byte("correct-password")
|
||||
hashed, err := bcrypt.GenerateFromPassword(plaintext, bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate bcrypt hash: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hashed []byte
|
||||
plaintext []byte
|
||||
wantMatch bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "correct password matches",
|
||||
hashed: hashed,
|
||||
plaintext: plaintext,
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "incorrect password does not match",
|
||||
hashed: hashed,
|
||||
plaintext: []byte("wrong-password"),
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "empty password does not match",
|
||||
hashed: hashed,
|
||||
plaintext: []byte(""),
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "invalid hash returns error",
|
||||
hashed: []byte("not-a-bcrypt-hash"),
|
||||
plaintext: plaintext,
|
||||
wantMatch: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
match, err := hasher.Compare(tt.hashed, tt.plaintext)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Compare() should return error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() error: %v", err)
|
||||
}
|
||||
if match != tt.wantMatch {
|
||||
t.Errorf("Compare() = %v, want %v", match, tt.wantMatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHashHash(t *testing.T) {
|
||||
hasher := BcryptHash{}
|
||||
plaintext := []byte("test-password")
|
||||
hashed, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error: %v", err)
|
||||
}
|
||||
if len(hashed) == 0 {
|
||||
t.Fatal("Hash() returned empty result")
|
||||
}
|
||||
|
||||
// Verify the hash is valid bcrypt
|
||||
err = bcrypt.CompareHashAndPassword(hashed, plaintext)
|
||||
if err != nil {
|
||||
t.Errorf("Hash() produced invalid bcrypt hash: %v", err)
|
||||
}
|
||||
|
||||
// Verify re-hashing produces different output (different salt)
|
||||
hashed2, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() second call error: %v", err)
|
||||
}
|
||||
if string(hashed) == string(hashed2) {
|
||||
t.Error("Hash() should produce different output on each call (different salt)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHashRoundTrip(t *testing.T) {
|
||||
hasher := BcryptHash{}
|
||||
plaintext := []byte("round-trip-test")
|
||||
hashed, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error: %v", err)
|
||||
}
|
||||
match, err := hasher.Compare(hashed, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() error: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Error("Hash then Compare round-trip failed: password should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptFakeHash(t *testing.T) {
|
||||
hasher := BcryptHash{}
|
||||
fake := hasher.FakeHash()
|
||||
if len(fake) == 0 {
|
||||
t.Fatal("FakeHash() returned empty result")
|
||||
}
|
||||
|
||||
// FakeHash should be a valid bcrypt hash matching "antitiming"
|
||||
err := bcrypt.CompareHashAndPassword(fake, []byte("antitiming"))
|
||||
if err != nil {
|
||||
t.Errorf("FakeHash() is not a valid bcrypt hash of 'antitiming': %v", err)
|
||||
}
|
||||
|
||||
// Calling FakeHash multiple times should return the same value
|
||||
fake2 := hasher.FakeHash()
|
||||
if string(fake) != string(fake2) {
|
||||
t.Error("FakeHash() should return constant value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHashWithCustomCost(t *testing.T) {
|
||||
hasher := BcryptHash{cost: bcrypt.MinCost}
|
||||
plaintext := []byte("low-cost-test")
|
||||
hashed, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() with MinCost error: %v", err)
|
||||
}
|
||||
match, err := hasher.Compare(hashed, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() error: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Error("Hash/Compare with MinCost should match")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user