diff --git a/internal/sbi/api_ueauthentication.go b/internal/sbi/api_ueauthentication.go index f58795a4..86fa8771 100644 --- a/internal/sbi/api_ueauthentication.go +++ b/internal/sbi/api_ueauthentication.go @@ -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" ) @@ -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{ @@ -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") @@ -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 { @@ -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) } diff --git a/internal/util/suci.go b/internal/util/suci.go new file mode 100644 index 00000000..93cecfea --- /dev/null +++ b/internal/util/suci.go @@ -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------ +// 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----- +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 +} diff --git a/internal/util/suci_test.go b/internal/util/suci_test.go new file mode 100644 index 00000000..5fd508cf --- /dev/null +++ b/internal/util/suci_test.go @@ -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------ + // 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----- + // ========================================== + 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) + }) +} diff --git a/internal/util/supi.go b/internal/util/supi.go new file mode 100644 index 00000000..cfaf0789 --- /dev/null +++ b/internal/util/supi.go @@ -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 +} diff --git a/internal/util/supi_test.go b/internal/util/supi_test.go new file mode 100644 index 00000000..fdb76278 --- /dev/null +++ b/internal/util/supi_test.go @@ -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) + }) +}