Skip to content

Commit 1d2fb22

Browse files
committed
chore: wrap ngksi increment in helper and add unit tests
Signed-off-by: Guillaume Belanger <guillaume.belanger27@gmail.com>
1 parent 9bb8c8b commit 1d2fb22

5 files changed

Lines changed: 183 additions & 10 deletions

File tree

internal/amf/nas/gmm/common.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import (
44
"github.com/ellanetworks/core/internal/models"
55
)
66

7+
// nextNgKsi returns the next available NAS Key Set Identifier.
8+
// KSI is a 3-bit field (0–6 valid, 7 means "no key available").
9+
// See 3GPP TS 24.501 section 9.11.3.32.
10+
func nextNgKsi(current int32) int32 {
11+
if current >= 0 && current < 6 {
12+
return current + 1
13+
}
14+
15+
return 0
16+
}
17+
718
func plmnIDStringToModels(plmnIDStr string) models.PlmnID {
819
if len(plmnIDStr) < 5 {
920
return models.PlmnID{}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package gmm
2+
3+
import "testing"
4+
5+
func TestNextNgKsi(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
current int32
9+
want int32
10+
}{
11+
{"0 -> 1", 0, 1},
12+
{"3 -> 4", 3, 4},
13+
{"5 -> 6", 5, 6},
14+
{"6 wraps to 0", 6, 0},
15+
{"7 (no key) wraps to 0", 7, 0},
16+
{"8 (out of range) wraps to 0", 8, 0},
17+
{"negative wraps to 0", -1, 0},
18+
}
19+
20+
for _, tt := range tests {
21+
t.Run(tt.name, func(t *testing.T) {
22+
got := nextNgKsi(tt.current)
23+
if got != tt.want {
24+
t.Errorf("nextNgKsi(%d) = %d, want %d", tt.current, got, tt.want)
25+
}
26+
})
27+
}
28+
}

internal/amf/nas/gmm/handle_authentication_failure.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,7 @@ func handleAuthenticationFailure(ctx context.Context, amfInstance *amf.AMF, ue *
4646
ue.Log.Warn("Authentication Failure Cause: NgKSI Already In Use")
4747
ue.AuthFailureCauseSynchFailureTimes = 0
4848
ue.Log.Warn("Select new NgKsi")
49-
// select new ngksi
50-
if ue.NgKsi.Ksi < 6 { // ksi is range from 0 to 6
51-
ue.NgKsi.Ksi += 1
52-
} else {
53-
ue.NgKsi.Ksi = 0
54-
}
49+
ue.NgKsi.Ksi = nextNgKsi(ue.NgKsi.Ksi)
5550

5651
err := message.SendAuthenticationRequest(ctx, amfInstance, ue.RanUe())
5752
if err != nil {

internal/amf/nas/gmm/handle_registration_request.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,8 @@ func handleRegistrationRequestMessage(ctx context.Context, amfInstance *amf.AMF,
163163
ue.NgKsi.Tsc = models.ScTypeMapped
164164
}
165165

166-
ue.NgKsi.Ksi = int32(registrationRequest.NgksiAndRegistrationType5GS.GetNasKeySetIdentifiler()) + 1
167-
if ue.NgKsi.Tsc == models.ScTypeNative && ue.NgKsi.Ksi != 7 {
168-
} else {
166+
ue.NgKsi.Ksi = nextNgKsi(int32(registrationRequest.NgksiAndRegistrationType5GS.GetNasKeySetIdentifiler()))
167+
if ue.NgKsi.Tsc != models.ScTypeNative || ue.NgKsi.Ksi == 7 {
169168
ue.NgKsi.Tsc = models.ScTypeNative
170169
ue.NgKsi.Ksi = 0
171170
}

internal/amf/nas/gmm/handle_registration_request_test.go

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,15 +858,155 @@ func TestHandleRegistrationRequest_CipheredNAS_MacFailed_SkipContainer(t *testin
858858
}
859859
}
860860

861+
// TestHandleRegistrationRequest_NgKsi_Increment validates that the AMF
862+
// allocates the next available ngKSI slot (current + 1) to avoid reusing
863+
// the UE's active key, per 3GPP TS 24.501 section 9.11.3.32.
864+
func TestHandleRegistrationRequest_NgKsi_Increment(t *testing.T) {
865+
ctx := context.TODO()
866+
amfInstance := amf.New(&FakeDBInstance{
867+
Operator: &db.Operator{
868+
Mcc: "001",
869+
Mnc: "01",
870+
Sst: 1,
871+
SupportedTACs: "[\"000001\"]",
872+
},
873+
}, &FakeAusf{
874+
AvKgAka: &ausf.AuthResult{
875+
Rand: hex.EncodeToString(make([]byte, 16)),
876+
Autn: hex.EncodeToString(make([]byte, 16)),
877+
},
878+
Supi: mustSUPIFromPrefixed("imsi-001019756139935"),
879+
Kseaf: "testkey",
880+
}, nil)
881+
882+
ue, _, err := buildUeAndRadio()
883+
if err != nil {
884+
t.Fatalf("could not create UE and radio: %v", err)
885+
}
886+
887+
ue.Suci = "testsuci"
888+
ue.Supi = mustSUPIFromPrefixed("imsi-001019756139935")
889+
890+
m, err := buildTestRegistrationRequestMessageWithNgKsi(0, nil, 0, 3)
891+
if err != nil {
892+
t.Fatalf("could not build registration request message: %v", err)
893+
}
894+
895+
err = handleRegistrationRequest(ctx, amfInstance, ue, m)
896+
if err != nil {
897+
t.Fatalf("registration request should be accepted, got: %v", err)
898+
}
899+
900+
if ue.NgKsi.Ksi != 4 {
901+
t.Fatalf("expected ngKSI=4 (next after 3), got %d", ue.NgKsi.Ksi)
902+
}
903+
}
904+
905+
// TestHandleRegistrationRequest_NgKsi_WrapAt6 validates that when the UE sends
906+
// ngKSI=6 (the maximum valid value), the AMF wraps around to 0 rather than
907+
// using value 7 (which means "no key available").
908+
func TestHandleRegistrationRequest_NgKsi_WrapAt6(t *testing.T) {
909+
ctx := context.TODO()
910+
amfInstance := amf.New(&FakeDBInstance{
911+
Operator: &db.Operator{
912+
Mcc: "001",
913+
Mnc: "01",
914+
Sst: 1,
915+
SupportedTACs: "[\"000001\"]",
916+
},
917+
}, &FakeAusf{
918+
AvKgAka: &ausf.AuthResult{
919+
Rand: hex.EncodeToString(make([]byte, 16)),
920+
Autn: hex.EncodeToString(make([]byte, 16)),
921+
},
922+
Supi: mustSUPIFromPrefixed("imsi-001019756139935"),
923+
Kseaf: "testkey",
924+
}, nil)
925+
926+
ue, _, err := buildUeAndRadio()
927+
if err != nil {
928+
t.Fatalf("could not create UE and radio: %v", err)
929+
}
930+
931+
ue.Suci = "testsuci"
932+
ue.Supi = mustSUPIFromPrefixed("imsi-001019756139935")
933+
934+
m, err := buildTestRegistrationRequestMessageWithNgKsi(0, nil, 0, 6)
935+
if err != nil {
936+
t.Fatalf("could not build registration request message: %v", err)
937+
}
938+
939+
err = handleRegistrationRequest(ctx, amfInstance, ue, m)
940+
if err != nil {
941+
t.Fatalf("registration request should be accepted, got: %v", err)
942+
}
943+
944+
if ue.NgKsi.Ksi != 0 {
945+
t.Fatalf("expected ngKSI=0 (wrapped from 6), got %d", ue.NgKsi.Ksi)
946+
}
947+
}
948+
949+
// TestHandleRegistrationRequest_NgKsi_NoKeyAvailable validates that when the UE
950+
// sends ngKSI=7 ("no key available"), the AMF handles it gracefully by
951+
// resetting to 0 with native security context type.
952+
func TestHandleRegistrationRequest_NgKsi_NoKeyAvailable(t *testing.T) {
953+
ctx := context.TODO()
954+
amfInstance := amf.New(&FakeDBInstance{
955+
Operator: &db.Operator{
956+
Mcc: "001",
957+
Mnc: "01",
958+
Sst: 1,
959+
SupportedTACs: "[\"000001\"]",
960+
},
961+
}, &FakeAusf{
962+
AvKgAka: &ausf.AuthResult{
963+
Rand: hex.EncodeToString(make([]byte, 16)),
964+
Autn: hex.EncodeToString(make([]byte, 16)),
965+
},
966+
Supi: mustSUPIFromPrefixed("imsi-001019756139935"),
967+
Kseaf: "testkey",
968+
}, nil)
969+
970+
ue, _, err := buildUeAndRadio()
971+
if err != nil {
972+
t.Fatalf("could not create UE and radio: %v", err)
973+
}
974+
975+
ue.Suci = "testsuci"
976+
ue.Supi = mustSUPIFromPrefixed("imsi-001019756139935")
977+
978+
m, err := buildTestRegistrationRequestMessageWithNgKsi(0, nil, 0, 7)
979+
if err != nil {
980+
t.Fatalf("could not build registration request message: %v", err)
981+
}
982+
983+
err = handleRegistrationRequest(ctx, amfInstance, ue, m)
984+
if err != nil {
985+
t.Fatalf("registration request should be accepted, got: %v", err)
986+
}
987+
988+
if ue.NgKsi.Ksi != 0 {
989+
t.Fatalf("expected ngKSI=0 (reset from no-key-available=7), got %d", ue.NgKsi.Ksi)
990+
}
991+
992+
if ue.NgKsi.Tsc != models.ScTypeNative {
993+
t.Fatalf("expected TSC=NATIVE, got %v", ue.NgKsi.Tsc)
994+
}
995+
}
996+
861997
func buildTestRegistrationRequestMessage(cipherAlg uint8, key *[16]uint8, ulcount uint32) (*nas.GmmMessage, error) {
998+
return buildTestRegistrationRequestMessageWithNgKsi(cipherAlg, key, ulcount, 0)
999+
}
1000+
1001+
func buildTestRegistrationRequestMessageWithNgKsi(cipherAlg uint8, key *[16]uint8, ulcount uint32, ngKsi uint8) (*nas.GmmMessage, error) {
8621002
m := nas.NewGmmMessage()
8631003

8641004
registrationRequest := nasMessage.NewRegistrationRequest(0)
8651005
registrationRequest.SetExtendedProtocolDiscriminator(nasMessage.Epd5GSMobilityManagementMessage)
8661006
registrationRequest.SetSecurityHeaderType(nas.SecurityHeaderTypePlainNas)
8671007
registrationRequest.SetSpareHalfOctet(0x00)
8681008
registrationRequest.SetMessageType(nas.MsgTypeRegistrationRequest)
869-
registrationRequest.NgksiAndRegistrationType5GS.SetNasKeySetIdentifiler(uint8(0))
1009+
registrationRequest.NgksiAndRegistrationType5GS.SetNasKeySetIdentifiler(ngKsi)
8701010
registrationRequest.SetRegistrationType5GS(nasMessage.RegistrationType5GSInitialRegistration)
8711011
registrationRequest.SetFOR(1)
8721012
registrationRequest.MobileIdentity5GS = nasType.MobileIdentity5GS{

0 commit comments

Comments
 (0)