Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,130 @@
package jwt_utils
import (
"time"
"github.com/awnumar/memguard"
jwt "github.com/golang-jwt/jwt/v5"
)
// GenerateJWTToken Generate the `access token` for the secret key.
// SECURITY: HMAC secret is wiped from memory after signing to prevent memory dump attacks.
func GenerateJWTToken(hmacSecret []byte, uuid string, ad time.Duration) (string, time.Time, error) {
// SECURITY: Create a copy of the secret and wipe the copy after use
// Note: The original hmacSecret is owned by the caller
secretCopy := make([]byte, len(hmacSecret))
copy(secretCopy, hmacSecret)
defer memguard.WipeBytes(secretCopy) // SECURITY: Wipe secret copy after signing
token := jwt.New(jwt.SigningMethodHS256)
expiresIn := time.Now().Add(ad)
// CWE-391: Safe type assertion even though we just created the token
// Defensive programming to prevent future panics if jwt library changes
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", expiresIn, jwt.ErrTokenInvalidClaims
}
claims["session_uuid"] = uuid
claims["exp"] = expiresIn.Unix()
tokenString, err := token.SignedString(secretCopy)
if err != nil {
return "", expiresIn, err
}
return tokenString, expiresIn, nil
}
// GenerateJWTTokenPair Generate the `access token` and `refresh token` for the secret key.
// SECURITY: HMAC secret is wiped from memory after signing to prevent memory dump attacks.
func GenerateJWTTokenPair(hmacSecret []byte, uuid string, ad time.Duration, rd time.Duration) (string, time.Time, string, time.Time, error) {
// SECURITY: Create a copy of the secret and wipe the copy after use
secretCopy := make([]byte, len(hmacSecret))
copy(secretCopy, hmacSecret)
defer memguard.WipeBytes(secretCopy) // SECURITY: Wipe secret copy after signing
//
// Generate token.
//
token := jwt.New(jwt.SigningMethodHS256)
expiresIn := time.Now().Add(ad)
// CWE-391: Safe type assertion even though we just created the token
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", time.Now(), "", time.Now(), jwt.ErrTokenInvalidClaims
}
claims["session_uuid"] = uuid
claims["exp"] = expiresIn.Unix()
tokenString, err := token.SignedString(secretCopy)
if err != nil {
return "", time.Now(), "", time.Now(), err
}
//
// Generate refresh token.
//
refreshToken := jwt.New(jwt.SigningMethodHS256)
refreshExpiresIn := time.Now().Add(rd)
// CWE-391: Safe type assertion for refresh token
rtClaims, ok := refreshToken.Claims.(jwt.MapClaims)
if !ok {
return "", time.Now(), "", time.Now(), jwt.ErrTokenInvalidClaims
}
rtClaims["session_uuid"] = uuid
rtClaims["exp"] = refreshExpiresIn.Unix()
refreshTokenString, err := refreshToken.SignedString(secretCopy)
if err != nil {
return "", time.Now(), "", time.Now(), err
}
return tokenString, expiresIn, refreshTokenString, refreshExpiresIn, nil
}
// ProcessJWTToken validates either the `access token` or `refresh token` and returns either the `uuid` if success or error on failure.
// CWE-347: Implements proper algorithm validation to prevent JWT algorithm confusion attacks
// OWASP A02:2021: Cryptographic Failures - Prevents token forgery through algorithm switching
// SECURITY: HMAC secret copy is wiped from memory after validation.
func ProcessJWTToken(hmacSecret []byte, reqToken string) (string, error) {
// SECURITY: Create a copy of the secret and wipe the copy after use
secretCopy := make([]byte, len(hmacSecret))
copy(secretCopy, hmacSecret)
defer memguard.WipeBytes(secretCopy) // SECURITY: Wipe secret copy after validation
token, err := jwt.Parse(reqToken, func(t *jwt.Token) (any, error) {
// CRITICAL SECURITY FIX: Validate signing method to prevent algorithm confusion attacks
// Protects against:
// 1. "none" algorithm bypass (CVE-2015-9235)
// 2. HS256/RS256 algorithm confusion (CVE-2016-5431)
// 3. Token forgery through algorithm switching
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrTokenSignatureInvalid
}
// Additional check: Ensure it's specifically HS256
if t.Method.Alg() != "HS256" {
return nil, jwt.ErrTokenSignatureInvalid
}
return secretCopy, nil
})
if err == nil && token.Valid {
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Safe type assertion with validation
sessionUUID, ok := claims["session_uuid"].(string)
if !ok {
return "", jwt.ErrTokenInvalidClaims
}
return sessionUUID, nil
}
return "", err
}
return "", err
}

View file

@ -0,0 +1,194 @@
package jwt_utils
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var testSecret = []byte("test-secret-key")
func TestGenerateJWTToken(t *testing.T) {
uuid := "test-uuid"
duration := time.Hour
token, expiry, err := GenerateJWTToken(testSecret, uuid, duration)
assert.NoError(t, err)
assert.NotEmpty(t, token)
assert.True(t, expiry.After(time.Now()))
assert.True(t, expiry.Before(time.Now().Add(duration).Add(time.Second)))
// Verify token can be processed
processedUUID, err := ProcessJWTToken(testSecret, token)
assert.NoError(t, err)
assert.Equal(t, uuid, processedUUID)
}
func TestGenerateJWTTokenPair(t *testing.T) {
uuid := "test-uuid"
accessDuration := time.Hour
refreshDuration := time.Hour * 24
accessToken, accessExpiry, refreshToken, refreshExpiry, err := GenerateJWTTokenPair(
testSecret,
uuid,
accessDuration,
refreshDuration,
)
assert.NoError(t, err)
assert.NotEmpty(t, accessToken)
assert.NotEmpty(t, refreshToken)
assert.True(t, accessExpiry.After(time.Now()))
assert.True(t, refreshExpiry.After(time.Now()))
assert.True(t, accessExpiry.Before(time.Now().Add(accessDuration).Add(time.Second)))
assert.True(t, refreshExpiry.Before(time.Now().Add(refreshDuration).Add(time.Second)))
// Verify both tokens can be processed
processedAccessUUID, err := ProcessJWTToken(testSecret, accessToken)
assert.NoError(t, err)
assert.Equal(t, uuid, processedAccessUUID)
processedRefreshUUID, err := ProcessJWTToken(testSecret, refreshToken)
assert.NoError(t, err)
assert.Equal(t, uuid, processedRefreshUUID)
}
func TestProcessJWTToken_Invalid(t *testing.T) {
tests := []struct {
name string
token string
wantErr bool
}{
{
name: "empty token",
token: "",
wantErr: true,
},
{
name: "malformed token",
token: "not.a.token",
wantErr: true,
},
{
name: "wrong signature",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiJ0ZXN0LXV1aWQiLCJleHAiOjE3MDQwNjc1NTF9.wrong",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
uuid, err := ProcessJWTToken(testSecret, tt.token)
if tt.wantErr {
assert.Error(t, err)
assert.Empty(t, uuid)
} else {
assert.NoError(t, err)
assert.NotEmpty(t, uuid)
}
})
}
}
func TestProcessJWTToken_Expired(t *testing.T) {
uuid := "test-uuid"
duration := -time.Hour // negative duration for expired token
token, _, err := GenerateJWTToken(testSecret, uuid, duration)
assert.NoError(t, err)
processedUUID, err := ProcessJWTToken(testSecret, token)
assert.Error(t, err)
assert.Empty(t, processedUUID)
}
// TestProcessJWTToken_AlgorithmConfusion tests protection against JWT algorithm confusion attacks
// CVE-2015-9235: None algorithm bypass
// CVE-2016-5431: HS256/RS256 algorithm confusion
// CWE-347: Improper Verification of Cryptographic Signature
func TestProcessJWTToken_AlgorithmConfusion(t *testing.T) {
tests := []struct {
name string
token string
description string
wantErr bool
}{
{
name: "none algorithm bypass attempt",
// Token with "alg": "none" - should be rejected
token: "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZXNzaW9uX3V1aWQiOiJhdHRhY2tlci11dWlkIiwiZXhwIjo5OTk5OTk5OTk5fQ.",
description: "Attacker tries to bypass signature verification using 'none' algorithm",
wantErr: true,
},
{
name: "RS256 algorithm confusion attempt",
// Token with "alg": "RS256" - should be rejected (we only accept HS256)
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiJhdHRhY2tlci11dWlkIiwiZXhwIjo5OTk5OTk5OTk5fQ.invalid",
description: "Attacker tries to use RS256 to confuse HMAC validation",
wantErr: true,
},
{
name: "HS384 algorithm attempt",
// Token with "alg": "HS384" - should be rejected (we only accept HS256)
token: "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiJhdHRhY2tlci11dWlkIiwiZXhwIjo5OTk5OTk5OTk5fQ.invalid",
description: "Attacker tries to use different HMAC algorithm",
wantErr: true,
},
{
name: "HS512 algorithm attempt",
// Token with "alg": "HS512" - should be rejected (we only accept HS256)
token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiJhdHRhY2tlci11dWlkIiwiZXhwIjo5OTk5OTk5OTk5fQ.invalid",
description: "Attacker tries to use different HMAC algorithm",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Logf("Testing: %s", tt.description)
uuid, err := ProcessJWTToken(testSecret, tt.token)
if tt.wantErr {
assert.Error(t, err, "Expected error for security vulnerability: %s", tt.description)
assert.Empty(t, uuid, "UUID should be empty when algorithm validation fails")
} else {
assert.NoError(t, err)
assert.NotEmpty(t, uuid)
}
})
}
}
// TestProcessJWTToken_ValidHS256Only tests that only valid HS256 tokens are accepted
func TestProcessJWTToken_ValidHS256Only(t *testing.T) {
uuid := "valid-test-uuid"
duration := time.Hour
// Generate a valid HS256 token
token, _, err := GenerateJWTToken(testSecret, uuid, duration)
assert.NoError(t, err, "Should generate valid token")
// Verify it's accepted
processedUUID, err := ProcessJWTToken(testSecret, token)
assert.NoError(t, err, "Valid HS256 token should be accepted")
assert.Equal(t, uuid, processedUUID, "UUID should match")
}
// TestProcessJWTToken_MissingSessionUUID tests protection against missing session_uuid claim
func TestProcessJWTToken_MissingSessionUUID(t *testing.T) {
// This test verifies the safe type assertion fix for CWE-391
// A token without session_uuid claim should return an error, not panic
// Note: We can't easily create such a token with our GenerateJWTToken function
// as it always includes session_uuid. In a real attack scenario, an attacker
// would craft such a token manually. This test documents the expected behavior.
// For now, we verify that a malformed token is properly rejected
malformedToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTl9.invalid"
uuid, err := ProcessJWTToken(testSecret, malformedToken)
assert.Error(t, err, "Token without session_uuid should be rejected")
assert.Empty(t, uuid, "UUID should be empty for invalid token")
}