Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions internal/sbi/api_ueauthentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/free5gc/openapi"
"github.com/free5gc/openapi/models"
"github.com/free5gc/udm/internal/logger"
"github.com/free5gc/udm/internal/util"
"github.com/free5gc/util/metrics/sbi"
)

Expand All @@ -25,6 +26,22 @@ func (s *Server) getUEAuthenticationRoutes() []Route {
// ConfirmAuth - Create a new confirmation event
func (s *Server) HandleConfirmAuth(c *gin.Context) {
var authEvent models.AuthEvent
// TS 29.503 6.3.6.2.3
// Validate SUPI format
supi := c.Params.ByName("supi")
if !util.IsValidSupi(supi) {
problemDetail := models.ProblemDetails{
Title: "Malformed request syntax",
Status: http.StatusBadRequest,
Detail: "Supi is invalid",
Cause: "INVALID_KEY",
}
logger.UeauLog.Warnln("Supi is invalid")
c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status)))
c.JSON(int(problemDetail.Status), problemDetail)
return
}

requestBody, err := c.GetRawData()
if err != nil {
problemDetail := models.ProblemDetails{
Expand Down Expand Up @@ -53,7 +70,30 @@ func (s *Server) HandleConfirmAuth(c *gin.Context) {
return
}

supi := c.Params.ByName("supi")
// TS 29.503 6.3.6.2.7 requirements check
missingIE := ""
if authEvent.NfInstanceId == "" {
missingIE = "nfInstanceId"
} else if authEvent.TimeStamp == nil {
missingIE = "timestamp"
} else if authEvent.AuthType == "" {
missingIE = "authtype"
} else if authEvent.ServingNetworkName == "" {
missingIE = "servingNetworkName"
}

if missingIE != "" {
problemDetail := models.ProblemDetails{
Title: "Missing or invalid parameter",
Status: http.StatusBadRequest,
Detail: "Mandatory IE " + missingIE + " is missing or invalid",
Cause: "MISSING_OR_INVALID_PARAMETER",
}
logger.UeauLog.Warnln("Mandatory IE " + missingIE + "is missing or invalid")
c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status)))
c.JSON(int(problemDetail.Status), problemDetail)
return
}

logger.UeauLog.Infoln("Handle ConfirmAuthDataRequest")

Expand All @@ -63,6 +103,21 @@ func (s *Server) HandleConfirmAuth(c *gin.Context) {
// GenerateAuthData - Generate authentication data for the UE
func (s *Server) HandleGenerateAuthData(c *gin.Context) {
var authInfoReq models.AuthenticationInfoRequest
// TS 29.503 6.3.3.2.2
// Validate SUPI or SUCI format
supiOrSuci := c.Param("supiOrSuci")
if !util.IsValidSupi(supiOrSuci) && !util.IsValidSuci(supiOrSuci) {
problemDetail := models.ProblemDetails{
Title: "Malformed request syntax",
Status: http.StatusBadRequest,
Detail: "Supi or Suci is invalid",
Cause: "INVALID_KEY",
}
logger.UeauLog.Warnln("Supi or Suci is invalid")
c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status)))
c.JSON(int(problemDetail.Status), problemDetail)
return
}

requestBody, err := c.GetRawData()
if err != nil {
Expand Down Expand Up @@ -92,9 +147,28 @@ func (s *Server) HandleGenerateAuthData(c *gin.Context) {
return
}

logger.UeauLog.Infoln("Handle GenerateAuthDataRequest")
// TS 29.503 6.3.6.2.2 requirements check
missingIE := ""
if authInfoReq.ServingNetworkName == "" {
missingIE = "servingNetworkName"
} else if authInfoReq.AusfInstanceId == "" {
missingIE = "ausfInstanceId"
}

supiOrSuci := c.Param("supiOrSuci")
if missingIE != "" {
problemDetail := models.ProblemDetails{
Title: "Missing or invalid parameter",
Status: http.StatusBadRequest,
Detail: "Mandatory IE " + missingIE + " is missing or invalid",
Cause: "MISSING_OR_INVALID_PARAMETER",
}
logger.UeauLog.Warnln("Mandatory IE " + missingIE + "is missing or invalid")
c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status)))
c.JSON(int(problemDetail.Status), problemDetail)
return
}

logger.UeauLog.Infoln("Handle GenerateAuthDataRequest")

s.Processor().GenerateAuthDataProcedure(c, authInfoReq, supiOrSuci)
}
Expand Down
46 changes: 46 additions & 0 deletions internal/util/suci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package util

import (
"regexp"
"strings"
)

// TS 29.571 5.3.2 & TS 23.003 Clause 6.3
// Regex for IMSI-based SUCI (Type 0)
// Format: suci-0-<MCC>-<MNC>-<RoutingInd>-<Scheme>-<KeyId>-<Output>
// Example: suci-0-208-93-0-0-0-1234567890 (Null Scheme)
var suciImsiRegex = regexp.MustCompile(`^suci-0-[0-9]{3}-[0-9]{2,3}-[0-9a-fA-F]{1,4}-` +
`[0-9a-fA-F]{1,2}-[0-9a-fA-F]{1,2}-.+$`)

// Regex for NAI-based SUCI (Type 1)
// Format: suci-1-<HomeNetworkId>-<RoutingInd>-<Scheme>-<KeyId>-<Output>
var suciNaiRegex = regexp.MustCompile(`^suci-1-.+-[0-9a-fA-F]{1,4}-[0-9a-fA-F]{1,2}-[0-9a-fA-F]{1,2}-.+$`)

// IsValidSuci checks if the given string is a valid SUCI
func IsValidSuci(suci string) bool {
if len(suci) == 0 {
return false
}

// must start with "suci-"
if !strings.HasPrefix(suci, "suci-") {
return false
}

// prevent null byte injection
if strings.Contains(suci, "\x00") {
return false
}

// validate IMSI-based SUCI (Type 0)
if strings.HasPrefix(suci, "suci-0-") {
return suciImsiRegex.MatchString(suci)
}

// validate NAI-based SUCI (Type 1)
if strings.HasPrefix(suci, "suci-1-") {
return suciNaiRegex.MatchString(suci)
}

return false
}
109 changes: 109 additions & 0 deletions internal/util/suci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package util

import (
"testing"
)

func TestIsValidSuci(t *testing.T) {
type args struct {
suci string
}
type testCase struct {
name string
args args
want bool
}

runTests := func(t *testing.T, tests []testCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidSuci(tt.args.suci); got != tt.want {
t.Errorf("IsValidSuci() = %v, want %v (input: %q)", got, tt.want, tt.args.suci)
}
})
}
}

// ==========================================
// 1. IMSI-based SUCI (Type 0)
// Format: suci-0-<MCC>-<MNC>-<Routing>-<Scheme>-<KeyId>-<Output>
// Source: TS 23.003 Clause 6.3
// ==========================================
t.Run("Check_SUCI_Type0_IMSI", func(t *testing.T) {
tests := []testCase{
// Happy Paths
{
"Valid Type 0 (Null Scheme)",
args{"suci-0-208-93-0-0-0-208930000000003"},
true,
},
{
"Valid Type 0 (Profile A, Long Routing)",
args{"suci-0-466-92-f001-1-1-ECCOutputHex..."},
true,
},
{
"Valid Type 0 (3-digit MNC)",
args{"suci-0-466-092-0-0-0-123456"},
true,
},

// Format Errors
{
"Invalid Type 0 (Bad MCC)",
args{"suci-0-20A-93-0-0-0-123"}, // MCC must be digits
false,
},
{
"Invalid Type 0 (Bad Routing Ind)",
args{"suci-0-208-93-GGGG-0-0-123"}, // Routing must be Hex
false,
},
{
"Invalid Type 0 (Missing Parts)",
args{"suci-0-208-93-0-0-123"}, // Missing KeyId
false,
},
}
runTests(t, tests)
})

// ==========================================
// 2. NAI-based SUCI (Type 1)
// Format: suci-1-<HomeNet>-<Routing>-<Scheme>-<KeyId>-<Output>
// ==========================================
t.Run("Check_SUCI_Type1_NAI", func(t *testing.T) {
tests := []testCase{
{
"Valid Type 1 (Standard)",
args{"suci-1-factory.local-0-0-0-user1"},
true,
},
{
"Invalid Type 1 (Bad Hex)",
args{"suci-1-domain-Z-0-0-output"}, // Routing must be Hex
false,
},
}
runTests(t, tests)
})

// ==========================================
// 3. Security & Edge Cases
// ==========================================
t.Run("Check_Security_EdgeCases", func(t *testing.T) {
tests := []testCase{
{"Empty String", args{""}, false},
{"Wrong Prefix", args{"suciX-0-208-93-0-0-0-1"}, false},
{"Unknown Type (Type 9)", args{"suci-9-208-93-0-0-0-1"}, false}, // Currently only 0 and 1 supported

// [Security] Null Byte Injection
{"Null Byte Injection", args{"suci-0-208-93\x00-0-0-0-1"}, false},

// Fuzzing garbage
{"Garbage String", args{"suci-0-garbage-data"}, false},
}
runTests(t, tests)
})
}
41 changes: 41 additions & 0 deletions internal/util/supi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package util

import (
"regexp"
"strings"
)

// TS 29.571 5.3.2 & TS 23.003
// SUPI format validation
var supiImsiRegex = regexp.MustCompile(`^imsi-[0-9]{5,15}$`)

// TS 29.571 5.3.2 & TS 23.003 28.7.2
// NAI format validation
var supiNaiRegex = regexp.MustCompile(`^nai-.+@.+$`)

// TS 29.571 5.3.2 & TS 23.003 28.15.2(gci) & TS 23.003 28.16.2(gli)
// GCI/GLI format validation
var supiGciGliRegex = regexp.MustCompile(`^(gci|gli)-.+$`)

// IsValidSupi checks if the given SUPI is valid according to 3GPP specifications
func IsValidSupi(supi string) bool {
if len(supi) == 0 {
return false
}

if strings.HasPrefix(supi, "imsi-") {
return supiImsiRegex.MatchString(supi)
}

if strings.HasPrefix(supi, "nai-") {
if strings.Contains(supi, "\x00") {
return false
}
return supiNaiRegex.MatchString(supi)
}
if strings.HasPrefix(supi, "gci-") || strings.HasPrefix(supi, "gli-") {
return supiGciGliRegex.MatchString(supi)
}

return false
}
75 changes: 75 additions & 0 deletions internal/util/supi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package util

import (
"testing"
)

func TestIsValidSupi(t *testing.T) {
type Args struct {
supi string
}

type testCase struct {
name string
Args Args
Want bool
}
runTests := func(t *testing.T, tests []testCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidSupi(tt.Args.supi); got != tt.Want {
t.Errorf("IsValidSupi() = %v, Want %v (input: %q)", got, tt.Want, tt.Args.supi)
}
})
}
}
// IMSI test
t.Run("Check_IMSI", func(t *testing.T) {
tests := []testCase{
{"Valid IMSI (15 digits)", Args{"imsi-208930000000003"}, true},
{"Valid IMSI (5 digits)", Args{"imsi-12345"}, true},
{"Invalid IMSI (Too short)", Args{"imsi-1234"}, false},
{"Invalid IMSI (Too long)", Args{"imsi-1234567890123456"}, false},
{"Invalid IMSI (Non-digits)", Args{"imsi-20893abc000003"}, false},
{"Invalid IMSI (Null Byte)", Args{"imsi-123\x00456"}, false},
}
runTests(t, tests)
})

// NAI test
t.Run("Check_NAI", func(t *testing.T) {
tests := []testCase{
{"Valid NAI (Standard)", Args{"nai-user@realm.com"}, true},
{"Valid NAI (3GPP Style)", Args{"nai-type0.rid123.schid0.userid1@5gc.mnc001.mcc001.org"}, true},
{"Invalid NAI (Missing @)", Args{"nai-userrealm.com"}, false},
{"Invalid NAI (Missing Realm)", Args{"nai-user@"}, false},
{"Invalid NAI (Missing User)", Args{"nai-@realm"}, false},
{"Security: NAI with Null Byte", Args{"nai-user\x00@realm"}, false},
}
runTests(t, tests)
})

// GCI/GLI test
t.Run("Check_GCI_GLI", func(t *testing.T) {
tests := []testCase{
{"Valid GCI", Args{"gci-cable-mac-1234"}, true},
{"Valid GLI", Args{"gli-fiber-line-5678"}, true},
{"Invalid GCI (Empty body)", Args{"gci-"}, false},
{"Invalid GLI (Empty body)", Args{"gli-"}, false},
}
runTests(t, tests)
})

// General invalid test
t.Run("Check_General_Invalid", func(t *testing.T) {
tests := []testCase{
{"Empty String", Args{""}, false},
{"Unknown Prefix", Args{"unknown-12345"}, false},
{"Just Prefix", Args{"imsi-"}, false},
{"Security: Raw Null Bytes", Args{"\x00\x00\x00"}, false},
{"Security: Garbage", Args{"fuzzing_payload"}, false},
}
runTests(t, tests)
})
}
Loading