1
0
mirror of https://github.com/H0llyW00dzZ/fiber2fa.git synced 2025-02-06 10:24:03 +00:00

Improve [HOTP & TOTP] Separate signature verification logic (#94)

- [+] refactor(otpverifier): separate signature verification logic and add GenerateToken method
- [+] feat(otpverifier): add support for verifying token without signature
- [+] test(otpverifier): add tests for missing signature and signature mismatch scenarios
This commit is contained in:
H0llyW00dzZ 2024-05-30 21:40:48 +07:00 committed by GitHub
parent 82159c8725
commit 0d47e07cc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 255 additions and 80 deletions

View File

@ -269,8 +269,8 @@ func TestMiddleware_Handle(t *testing.T) {
Secret: info.Secret,
})
validToken, _ := totp.GenerateToken()
totp.Verify(validToken, "")
validToken := totp.GenerateToken()
totp.Verify(validToken)
// Create a separate instance of the Middleware struct for testing
testMiddleware := &twofa.Middleware{
@ -1279,8 +1279,8 @@ func TestMiddlewareUUIDContextKey_Handle(t *testing.T) {
Secret: info.Secret,
})
validToken, _ := totp.GenerateToken()
totp.Verify(validToken, "")
validToken := totp.GenerateToken()
totp.Verify(validToken)
// Create a separate instance of the Middleware struct for testing
testMiddleware := &twofa.Middleware{

View File

@ -90,8 +90,8 @@ func BenchmarkJSONSonicWithValid2FA(b *testing.B) {
Secret: twoFAConfig.Secret,
})
token, _ := totp.GenerateToken()
totp.Verify(token, "")
token := totp.GenerateToken()
totp.Verify(token)
// Create a valid 2FA cookie
cookieValue := twoFAMiddleware.GenerateCookieValue(time.Now().Add(time.Duration(86400) * time.Second))
@ -154,8 +154,8 @@ func BenchmarkJSONSonicWithValidCookie(b *testing.B) {
Secret: twoFAConfig.Secret,
})
token, _ := totp.GenerateToken()
totp.Verify(token, "")
token := totp.GenerateToken()
totp.Verify(token)
// Create a valid 2FA cookie
cookieValue := twoFAMiddleware.GenerateCookieValue(time.Now().Add(time.Duration(86400) * time.Second))
@ -280,8 +280,8 @@ func BenchmarkJSONStdLibraryMiddlewareWithValid2FA(b *testing.B) {
Secret: twoFAConfig.Secret,
})
token, _ := totp.GenerateToken()
totp.Verify(token, "")
token := totp.GenerateToken()
totp.Verify(token)
// Create a valid 2FA cookie
cookieValue := twoFAMiddleware.GenerateCookieValue(time.Now().Add(time.Duration(86400) * time.Second))
@ -344,8 +344,8 @@ func BenchmarkJSONStdLibraryWithValidCookie(b *testing.B) {
Secret: twoFAConfig.Secret,
})
token, _ := totp.GenerateToken()
totp.Verify(token, "")
token := totp.GenerateToken()
totp.Verify(token)
// Create a valid 2FA cookie
cookieValue := twoFAMiddleware.GenerateCookieValue(time.Now().Add(time.Duration(86400) * time.Second))

View File

@ -41,8 +41,8 @@ func BenchmarkTOTPVerify(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Note: This now each token are different
token, _ := verifier.GenerateToken()
verifier.Verify(token, "")
token := verifier.GenerateToken()
verifier.Verify(token)
}
})
@ -58,7 +58,7 @@ func BenchmarkTOTPVerify(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Note: This now each token are different
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
verifier.Verify(token, signature)
}
})
@ -96,8 +96,8 @@ func BenchmarkHOTPVerify(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Note: This now each token are different
token, _ := verifier.GenerateToken()
verifier.Verify(token, "")
token := verifier.GenerateToken()
verifier.Verify(token)
}
})
@ -113,7 +113,7 @@ func BenchmarkHOTPVerify(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Note: This now each token are different
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
verifier.Verify(token, signature)
}
})

View File

@ -63,26 +63,31 @@ func NewHOTPVerifier(config ...Config) *HOTPVerifier {
// A successful verification will result in the counter being updated to the next expected value.
//
// Note: A firm grasp of the sync window concept is essential for understanding its role in the verification process.
func (v *HOTPVerifier) Verify(token, signature string) bool {
func (v *HOTPVerifier) Verify(token string, signature ...string) bool {
if v.config.SyncWindow < 0 {
panic("hotp: SyncWindow must be greater than or equal to zero")
}
if v.config.UseSignature {
if len(signature) == 0 {
panic("hotp: Signature is required but not provided")
}
return v.verifyWithSignature(token, signature[0])
}
return v.verifyWithoutSignature(token)
}
// verifyWithoutSignature checks if the provided token is valid for the specified counter value without signature verification.
func (v *HOTPVerifier) verifyWithoutSignature(token string) bool {
// Check if sync window is applied
// Note: Understanding this sync window requires skilled mathematical reasoning.
if v.config.SyncWindow > 0 {
for i := 0; i <= v.config.SyncWindow; i++ {
expectedCounter := int(v.config.Counter) + i
generatedToken := v.Hotp.At(expectedCounter)
tokenMatch := subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1
signatureMatch := true // Assume true if not using signatures.
if v.config.UseSignature {
generatedSignature := v.generateSignature(generatedToken)
signatureMatch = subtle.ConstantTimeCompare([]byte(signature), []byte(generatedSignature)) == 1
}
if tokenMatch && signatureMatch {
if subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1 {
// Update the stored counter to the next expected value after a successful match
v.config.Counter = uint64(expectedCounter + 1)
return true
@ -94,15 +99,7 @@ func (v *HOTPVerifier) Verify(token, signature string) bool {
// Default case when sync window is not applied
generatedToken := v.Hotp.At(int(v.config.Counter))
tokenMatch := subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1
signatureMatch := true // Assume true if not using signatures.
if v.config.UseSignature {
generatedSignature := v.generateSignature(generatedToken)
signatureMatch = subtle.ConstantTimeCompare([]byte(signature), []byte(generatedSignature)) == 1
}
if tokenMatch && signatureMatch {
if subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1 {
// Increment the counter value after successful verification
v.config.Counter++
return true
@ -110,13 +107,47 @@ func (v *HOTPVerifier) Verify(token, signature string) bool {
return false
}
// GenerateToken generates a token and signature for the current counter value.
func (v *HOTPVerifier) GenerateToken() (string, string) {
token := v.Hotp.At(int(v.config.Counter))
signature := ""
if v.config.UseSignature {
signature = v.generateSignature(token)
// verifyWithSignature checks if the provided token and signature are valid for the specified counter value.
func (v *HOTPVerifier) verifyWithSignature(token, signature string) bool {
// Check if sync window is applied
// Note: Understanding this sync window requires skilled mathematical reasoning.
if v.config.SyncWindow > 0 {
for i := 0; i <= v.config.SyncWindow; i++ {
expectedCounter := int(v.config.Counter) + i
generatedToken := v.Hotp.At(expectedCounter)
generatedSignature := v.generateSignature(generatedToken)
if subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1 &&
subtle.ConstantTimeCompare([]byte(signature), []byte(generatedSignature)) == 1 {
// Update the stored counter to the next expected value after a successful match
v.config.Counter = uint64(expectedCounter + 1)
return true
}
}
// If no match is found within the synchronization window, authentication fails
return false
}
// Default case when sync window is not applied
generatedToken := v.Hotp.At(int(v.config.Counter))
generatedSignature := v.generateSignature(generatedToken)
if subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1 &&
subtle.ConstantTimeCompare([]byte(signature), []byte(generatedSignature)) == 1 {
// Increment the counter value after successful verification
v.config.Counter++
return true
}
return false
}
// GenerateToken generates a token for the current counter value.
func (v *HOTPVerifier) GenerateToken() string {
return v.Hotp.At(int(v.config.Counter))
}
// GenerateTokenWithSignature generates a token and signature for the current counter value.
func (v *HOTPVerifier) GenerateTokenWithSignature() (string, string) {
token := v.Hotp.At(int(v.config.Counter))
signature := v.generateSignature(token)
return token, signature
}

View File

@ -111,8 +111,9 @@ type TimeSource func() time.Time
// OTPVerifier is an interface that defines the behavior of an OTP verifier.
type OTPVerifier interface {
Verify(token, signature string) bool
GenerateToken() (string, string)
Verify(token string, signature ...string) bool
GenerateToken() string
GenerateTokenWithSignature() (string, string)
SetCounter(counter uint64)
GetCounter() uint64
GenerateOTPURL(issuer, accountName string) string

View File

@ -63,7 +63,7 @@ func TestTOTPVerifier_Verify(t *testing.T) {
verifier := otpverifier.NewTOTPVerifier(config)
// Generate a token and signature using the verifier
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
// Verify the token and signature (should succeed)
isValid := verifier.Verify(token, signature)
@ -86,7 +86,7 @@ func TestTOTPVerifier_Verify(t *testing.T) {
// Switch to a non-signature mode and test again
config.UseSignature = false
verifier = otpverifier.NewTOTPVerifier(config)
token, _ = verifier.GenerateToken()
token, _ = verifier.GenerateTokenWithSignature()
// Verify the token without a signature (should succeed)
isValid = verifier.Verify(token, "")
@ -129,7 +129,7 @@ func TestDefaultConfigTOTPVerifier_Verify(t *testing.T) {
verifier := otpverifier.NewTOTPVerifier(config)
// Generate a token and signature using the verifier
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
// Verify the token and signature (should succeed)
isValid := verifier.Verify(token, signature)
@ -152,7 +152,7 @@ func TestDefaultConfigTOTPVerifier_Verify(t *testing.T) {
// Switch to a non-signature mode and test again
config.UseSignature = false
verifier = otpverifier.NewTOTPVerifier(config)
token, _ = verifier.GenerateToken()
token, _ = verifier.GenerateTokenWithSignature()
// Verify the token without a signature (should succeed)
isValid = verifier.Verify(token, "")
@ -183,8 +183,8 @@ func TestTOTPVerifier_PeriodicCleanup(t *testing.T) {
verifier := otpverifier.NewTOTPVerifier(config)
// Simulate used tokens
token1, _ := verifier.GenerateToken()
verifier.Verify(token1, "")
token1 := verifier.GenerateToken()
verifier.Verify(token1)
// Wait for periodic cleanup to occur (less than the token validity period)
time.Sleep(time.Duration(period*3/4) * time.Second)
@ -227,7 +227,7 @@ func TestHOTPVerifier_Verify(t *testing.T) {
verifier := otpverifier.NewHOTPVerifier(config)
// Generate a token and signature using the verifier
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
// Verify the token and signature
isValid := verifier.Verify(token, signature)
@ -239,7 +239,7 @@ func TestHOTPVerifier_Verify(t *testing.T) {
initialCounter++
config.Counter = initialCounter
verifier = otpverifier.NewHOTPVerifier(config)
newToken, newSignature := verifier.GenerateToken()
newToken, newSignature := verifier.GenerateTokenWithSignature()
// Verify the new token and signature
isValid = verifier.Verify(newToken, newSignature)
@ -258,10 +258,10 @@ func TestHOTPVerifier_Verify(t *testing.T) {
verifier = otpverifier.NewHOTPVerifier(config)
// Generate a token using the verifier
token, _ = verifier.GenerateToken()
token = verifier.GenerateToken()
// Verify the token
isValid = verifier.Verify(token, "")
isValid = verifier.Verify(token)
if !isValid {
t.Errorf("Token should be valid (hash function: %s)", hashFunc)
}
@ -299,7 +299,7 @@ func TestDefaultConfigHOTPVerifier_Verify(t *testing.T) {
verifier := otpverifier.NewHOTPVerifier(config)
// Generate a token and signature using the verifier
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
// Verify the token and signature
isValid := verifier.Verify(token, signature)
@ -311,7 +311,7 @@ func TestDefaultConfigHOTPVerifier_Verify(t *testing.T) {
initialCounter++
config.Counter = initialCounter
verifier = otpverifier.NewHOTPVerifier(config)
newToken, newSignature := verifier.GenerateToken()
newToken, newSignature := verifier.GenerateTokenWithSignature()
// Verify the new token and signature
isValid = verifier.Verify(newToken, newSignature)
@ -330,7 +330,7 @@ func TestDefaultConfigHOTPVerifier_Verify(t *testing.T) {
verifier = otpverifier.NewHOTPVerifier(config)
// Generate a token using the verifier
token, _ = verifier.GenerateToken()
token, _ = verifier.GenerateTokenWithSignature()
// Verify the token
isValid = verifier.Verify(token, "")
@ -390,7 +390,7 @@ func TestOTPFactory(t *testing.T) {
totpConfig.UseSignature = true
totpConfig.TimeSource = timeSource
totpVerifier = otpverifier.NewTOTPVerifier(totpConfig)
totpToken, totpSignature := totpVerifier.GenerateToken()
totpToken, totpSignature := totpVerifier.GenerateTokenWithSignature()
if !totpVerifier.Verify(totpToken, totpSignature) {
t.Errorf("TOTP token and signature should be valid (hash function: %s)", hashFunc)
}
@ -398,7 +398,7 @@ func TestOTPFactory(t *testing.T) {
// Test HOTPVerifier token generation and verification
hotpConfig.UseSignature = true
hotpVerifier = otpverifier.NewHOTPVerifier(hotpConfig)
hotpToken, hotpSignature := hotpVerifier.GenerateToken()
hotpToken, hotpSignature := hotpVerifier.GenerateTokenWithSignature()
if !hotpVerifier.Verify(hotpToken, hotpSignature) {
t.Errorf("HOTP token and signature should be valid (hash function: %s)", hashFunc)
}
@ -877,15 +877,17 @@ func TestTOTPVerifier_VerifyPanic(t *testing.T) {
// Create a TOTPVerifier with a negative sync window
config := otpverifier.Config{
Secret: secret,
SyncWindow: -1,
Secret: secret,
SyncWindow: -1,
UseSignature: true,
Hash: otpverifier.SHA256,
}
// Create a new TOTPVerifier instance
verifier := otpverifier.NewTOTPVerifier(config)
// Generate a token and signature using the verifier
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
// Expect a panic when calling Verify with a negative sync window
defer func() {
@ -910,13 +912,14 @@ func TestHOTPVerifier_VerifyPanic(t *testing.T) {
config := otpverifier.Config{
Secret: secret,
SyncWindow: -1,
Hash: otpverifier.SHA256,
}
// Create a new HOTPVerifier instance
verifier := otpverifier.NewHOTPVerifier(config)
// Generate a token and signature using the verifier
token, signature := verifier.GenerateToken()
token, signature := verifier.GenerateTokenWithSignature()
// Expect a panic when calling Verify with a negative sync window
defer func() {
@ -933,3 +936,95 @@ func TestHOTPVerifier_VerifyPanic(t *testing.T) {
// Call Verify, which should panic
verifier.Verify(token, signature)
}
func TestTOTPVerifier_VerifyMissingSignature(t *testing.T) {
secret := gotp.RandomSecret(16)
config := otpverifier.Config{
Secret: secret,
UseSignature: true,
Hash: otpverifier.SHA256,
}
verifier := otpverifier.NewTOTPVerifier(config)
token, _ := verifier.GenerateTokenWithSignature()
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected Verify to panic with missing signature, but it didn't")
} else {
expectedPanicMessage := "totp: Signature is required but not provided"
if r != expectedPanicMessage {
t.Errorf("Expected panic message: %s, but got: %s", expectedPanicMessage, r)
}
}
}()
verifier.Verify(token)
}
func TestTOTPVerifier_VerifySignatureMismatch(t *testing.T) {
secret := gotp.RandomSecret(16)
config := otpverifier.Config{
Secret: secret,
UseSignature: true,
Hash: otpverifier.SHA256,
}
verifier := otpverifier.NewTOTPVerifier(config)
token, _ := verifier.GenerateTokenWithSignature()
invalidSignature := "invalid_signature"
if verifier.Verify(token, invalidSignature) {
t.Errorf("Expected Verify to return false for signature mismatch, but it returned true")
}
}
func TestHOTPVerifier_VerifyMissingSignature(t *testing.T) {
secret := gotp.RandomSecret(16)
config := otpverifier.Config{
Secret: secret,
UseSignature: true,
Hash: otpverifier.SHA256,
}
verifier := otpverifier.NewHOTPVerifier(config)
token, _ := verifier.GenerateTokenWithSignature()
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected Verify to panic with missing signature, but it didn't")
} else {
expectedPanicMessage := "hotp: Signature is required but not provided"
if r != expectedPanicMessage {
t.Errorf("Expected panic message: %s, but got: %s", expectedPanicMessage, r)
}
}
}()
verifier.Verify(token)
}
func TestHOTPVerifier_VerifySignatureMismatch(t *testing.T) {
secret := gotp.RandomSecret(16)
config := otpverifier.Config{
Secret: secret,
UseSignature: true,
Hash: otpverifier.SHA256,
}
verifier := otpverifier.NewHOTPVerifier(config)
token, _ := verifier.GenerateTokenWithSignature()
invalidSignature := "invalid_signature"
if verifier.Verify(token, invalidSignature) {
t.Errorf("Expected Verify to return false for signature mismatch, but it returned true")
}
}

View File

@ -71,11 +71,59 @@ func NewTOTPVerifier(config ...Config) *TOTPVerifier {
//
// Note: This TOTP verification using [crypto/subtle] requires careful consideration
// when setting TimeSource and Period to ensure correct usage.
func (v *TOTPVerifier) Verify(token, signature string) bool {
func (v *TOTPVerifier) Verify(token string, signature ...string) bool {
if v.config.SyncWindow < 0 {
panic("totp: SyncWindow must be greater than or equal to zero")
}
if v.config.UseSignature {
if len(signature) == 0 {
panic("totp: Signature is required but not provided")
}
return v.verifyWithSignature(token, signature[0])
}
return v.verifyWithoutSignature(token)
}
// verifyWithoutSignature checks if the provided token is valid for the current time without signature verification.
func (v *TOTPVerifier) verifyWithoutSignature(token string) bool {
currentTimestamp := v.config.TimeSource().Unix()
currentTimeStep := currentTimestamp / int64(v.config.Period)
// Check the syncWindow periods before and after the current time step
for offset := -v.config.SyncWindow; offset <= v.config.SyncWindow; offset++ {
expectedTimeStep := currentTimeStep + int64(offset)
expectedTimestamp := expectedTimeStep * int64(v.config.Period)
v.m.Lock()
if _, found := v.UsedTokens[expectedTimeStep]; found {
v.m.Unlock()
continue // Skip this step as the token has already been used
}
v.m.Unlock()
// Verify the token for this time step
// This should be safe now because a synchronization window similar to HOTP is implemented.
// The token will be marked as used even if there is still time remaining in the period (e.g., 30 seconds).
// Without implementing a synchronization window similar to HOTP, this can lead to a high vulnerability
// where a used token is still considered valid due to the period.
generatedToken := v.totp.At(expectedTimestamp)
if subtle.ConstantTimeCompare([]byte(token), []byte(generatedToken)) == 1 {
v.m.Lock()
v.UsedTokens[expectedTimeStep] = token // Record the token as used
v.m.Unlock()
return true
}
}
return false // Token is invalid
}
// verifyWithSignature checks if the provided token and signature are valid for the current time.
func (v *TOTPVerifier) verifyWithSignature(token, signature string) bool {
currentTimestamp := v.config.TimeSource().Unix()
currentTimeStep := currentTimestamp / int64(v.config.Period)
@ -97,32 +145,32 @@ func (v *TOTPVerifier) Verify(token, signature string) bool {
// Without implementing a synchronization window similar to HOTP, this can lead to a high vulnerability
// where a used token is still considered valid due to the period.
if v.totp.Verify(token, expectedTimestamp) {
signatureMatch := true // Assume true if not using signatures.
if v.config.UseSignature {
generatedSignature := v.generateSignature(token)
signatureMatch = subtle.ConstantTimeCompare([]byte(signature), []byte(generatedSignature)) == 1
if subtle.ConstantTimeCompare([]byte(signature), []byte(generatedSignature)) != 1 {
return false // Signature mismatch
}
}
if signatureMatch {
v.m.Lock()
v.UsedTokens[expectedTimeStep] = token // Record the token as used
v.m.Unlock()
return true
}
v.m.Lock()
v.UsedTokens[expectedTimeStep] = token // Record the token as used
v.m.Unlock()
return true
}
}
return false // Token is invalid
}
// GenerateToken generates a token and signature for the current time.
func (v *TOTPVerifier) GenerateToken() (string, string) {
// GenerateToken generates a token for the current time.
func (v *TOTPVerifier) GenerateToken() string {
return v.totp.Now()
}
// GenerateTokenWithSignature generates a token and signature for the current time.
func (v *TOTPVerifier) GenerateTokenWithSignature() (string, string) {
token := v.totp.Now()
signature := ""
if v.config.UseSignature {
signature = v.generateSignature(token)
}
signature := v.generateSignature(token)
return token, signature
}