Free5GC原始碼研究(4) - AUSF研究

zrq96發表於2024-10-01

本文研究AUthentication Server Function (AUSF) 主要實現的功能

AUSF的概念

在開始研究其原始碼之前,我們需要先對AUSF有一些概念上的認識。AUSF的主要技術文件是TS29.509,規定了AUSF提供哪些服務,其對應的API介面有哪些。總的來說,AUSF就是幫助其他NF,比如AMF,來認證一個接入裝置。然而整個鑑權過程比較複雜,AUSF在其中起到主導作用,但也會需要其他NF,比如UDM的協助。

我們熟悉的使用者鑑權過程都是像提供一個使用者密碼、提供一個手機驗證碼,或者一個實物證件這樣的,來證明我是我自己。而在通訊網路中鑑權過程對我們來說則略顯陌生,用的是一種叫AKA(Authentication and Key Agreement)的方式:它要求接入裝置計算一個aka-challenge,而只有真正的裝置才能準確算出這個aka-challenge,如此一來就完成了鑑權過程。這個過程本質上還是用金鑰加密解密的過程。整個鑑權過程包含了而至少兩次相互通訊,第一次通訊是裝置向網路提供自己的身份資訊(就像輸入使用者名稱),網路端在資料庫裡找到裝置的相關資料後返回一個aka-challenge;第二次通訊是裝置向網路傳送自己對aka-challenge解出來的RES,網路端判斷這個RES是否正確,如果正確則返回鑑權成功資訊,否則返回鑑權失敗資訊。

通訊網路的鑑權過程

能完成以上鑑權過程的具體協議至少有兩種,一種是EAP-AKA協議,目前最新版的文件是RFC9048;一種是5G-AKA協議,定義在TS33.401文件裡。EAP-AKA是一個較為通用的認證協議,而5G-AKA則是專門為5G網路設計的協議,提供了更強的保護和更優的體驗。考慮到了使用者和裝置的多樣性。一些老舊裝置可能只支援EAP-AKA,而新裝置則可能支援5G-AKA,5G標準要求同時支援這兩種協議,以確保網路的順利迭代、提供不同級別的安全保護、和滿足不同使用者及裝置的需求。TS29.509定義了AUSF應當提供的服務:

img

可以看到,除了基本的裝置鑑權服務(ue-authentications)以外,AUSF還應該提供SOR和UPU的保護,也就是提供訊息認證和完整性保護機制,這些服務確保了5G網路在漫遊和使用者面引數更新等關鍵操作中的安全性和可靠性,對於維護5G網路的穩定執行和保護使用者隱私至關重要。然而目前的v3.4.3版本中free5gc/ausf@v
1.2.3
尚未實現這些功能。

// https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/api_sorprotection.go
func (s *Server) SupiUeSorPost(c *gin.Context) {
	c.JSON(http.StatusNotImplemented, gin.H{})
}
// https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/api_upuprotection.go
func (s *Server) SupiUeUpuPost(c *gin.Context) {
	c.JSON(http.StatusNotImplemented, gin.H{})
}

因此,我們只需聚焦於研究裝置鑑權服務即可。

AUSF的實現

AUSF的設計文件描繪了為其設計的軟體架構(函式呼叫關係圖):

img

整個圖裡面我們需要重點關注的函式是紅框中的三個函式,它們才是實際解決鑑權問題的函式,其他都是簡單處理或者套殼呼叫。但在深入研究這三個函式以前,有必要先瞅一眼internal/context/裡面定義的資料結構,因為我們在前文已經知道,Context型別都是NF整個證明週期裡最核心的資料儲存中心。

// https://github.com/free5gc/ausf/blob/v1.2.3/internal/context/context.go

type AUSFContext struct {
    // other fields ......

	suciSupiMap          sync.Map
	UePool               sync.Map
	UdmUeauUrl           string
	EapAkaSupiImsiPrefix bool
}

type AusfUeContext struct {
	Supi               string
	Kausf              string
	Kseaf              string
	ServingNetworkName string
	AuthStatus         models.AuthResult
	UdmUeauUrl         string

	// for 5G AKA
	XresStar string

	// for EAP-AKA'
	K_aut    string
	XRES     string
	Rand     string
	EapID    uint8
	Resynced bool
}

context.go裡定義了各種型別,其中最值得留意的自然是AUSFContextAusfUeContext,前者儲存整個AUSF的重要資料,後者儲存一次裝置鑑權過程需要用到的資料。每一個裝置都和一個AusfUeContext對應,儲存在AUSFContext.UePool中。而AusfUeContext中的裝置ID都是SUPI(Subscription Permanent Identifier),然而為了保護使用者隱私,5G網路允許裝置在提供自身身份資訊時傳送SUCI(Subscription Concealed Identifier)。SUCI是一種隱藏或加密了的使用者訂閱識別符號,網路端在收到SUCI後要還原成SUPI才能進行其他操作,而這SCUI及其還原後的SUPI則儲存在AUSFContext.suciSupiMap結構裡,方便後續使用。

UeAuthPostRequestProcedure

簡單瞭解過這兩個Context型別後,我們開始聚焦UeAuthPostRequestProcedure函式,也就是裝置在鑑權過程第一步,訪問/ue-authentications時網路端的處理過程。下面是其大幅簡化後的程式碼:

// https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/processor/ue_authentication.go#L216
func (p *Processor) UeAuthPostRequestProcedure(c *gin.Context, updateAuthenticationInfo models.AuthenticationInfo) {
	supiOrSuci := updateAuthenticationInfo.SupiOrSuci
	result, err, pd := p.Consumer().GenerateAuthDataApi(udmUrl, supiOrSuci, authInfoReq)
	authInfoResult := *result
    ueid := authInfoResult.Supi
    ausfUeContext := ausf_context.NewAusfUeContext(ueid)
    ausf_context.AddAusfUeContextToPool(ausfUeContext)
    ausf_context.AddSuciSupiPairToMap(supiOrSuci, ueid)

	if authInfoResult.AuthType == models.AuthType__5_G_AKA {
        ausfUeContext.XresStar = authInfoResult.AuthenticationVector.XresStar
        //  av5gAka := encode5gAka(authInfoResult.AuthenticationVector)
		responseBody.Var5gAuthData = av5gAka
	} else if authInfoResult.AuthType == models.AuthType_EAP_AKA_PRIME {	
        ausfUeContext.XRES = authInfoResult.AuthenticationVector.Xres
		//  encodedPktAfterMAC := encodePacketArray(authInfoResult.AuthenticationVector)
		responseBody.Var5gAuthData = base64.StdEncoding.EncodeToString(encodedPktAfterMAC)
	}
	responseBody.AuthType = authInfoResult.AuthType
	c.JSON(http.StatusCreated, responseBody) // 返回裝置一個aka-challenge
}

整個函式所做的事情就是生成一個aka-challenge (responseBody.Var5gAuthData)和對應的Xres/XresStar,把Xres儲存在相應的ausfUeContext裡,然後把生成的aka-challenge返回給使用者裝置讓它計算出一個解來。這裡面生成aka-challenge的主要工作是用函式GenerateAuthDataApi呼叫UDM來實際完成的。其具體的做法,還要等我們以後研究UDM在看。另外,整個鑑權過程的第一步還用到了很多在free5gc/openapi定義的型別,瞭解這些型別的細節能進一步提高我們的理解水平。

點選檢視更多openapi裡的型別
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_authentication_info_request.go */
type AuthenticationInfo struct {
	SupiOrSuci            string                 `json:"supiOrSuci" yaml:"supiOrSuci" bson:"supiOrSuci"`
	ServingNetworkName    string                 `json:"servingNetworkName" yaml:"servingNetworkName" bson:"servingNetworkName"`
	ResynchronizationInfo *ResynchronizationInfo `json:"resynchronizationInfo,omitempty" yaml:"resynchronizationInfo" bson:"resynchronizationInfo"`
	TraceData             *TraceData             `json:"traceData,omitempty" yaml:"traceData" bson:"traceData"`
}

/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_ue_authentication_ctx.go */
type UeAuthenticationCtx struct {
	AuthType           AuthType                    `json:"authType" yaml:"authType" bson:"authType"`
	Var5gAuthData      interface{}                 `json:"5gAuthData" yaml:"5gAuthData" bson:"5gAuthData"`
	Links              map[string]LinksValueSchema `json:"_links" yaml:"_links" bson:"_links"`
	ServingNetworkName string                      `json:"servingNetworkName,omitempty" yaml:"servingNetworkName" bson:"servingNetworkName"`
}

/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_authentication_info_request.go */
type AuthenticationInfoRequest struct {
	SupportedFeatures     string                 `json:"supportedFeatures,omitempty" yaml:"supportedFeatures" bson:"supportedFeatures" mapstructure:"SupportedFeatures"`
	ServingNetworkName    string                 `json:"servingNetworkName" yaml:"servingNetworkName" bson:"servingNetworkName" mapstructure:"ServingNetworkName"`
	ResynchronizationInfo *ResynchronizationInfo `json:"resynchronizationInfo,omitempty" yaml:"resynchronizationInfo" bson:"resynchronizationInfo" mapstructure:"ResynchronizationInfo"`
	AusfInstanceId        string                 `json:"ausfInstanceId" yaml:"ausfInstanceId" bson:"ausfInstanceId" mapstructure:"AusfInstanceId"`
}

/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_authentication_info_result.go */
type AuthenticationInfoResult struct {
	AuthType             AuthType              `json:"authType" yaml:"authType" bson:"authType" mapstructure:"AuthType"`
	SupportedFeatures    string                `json:"supportedFeatures,omitempty" yaml:"supportedFeatures" bson:"supportedFeatures" mapstructure:"SupportedFeatures"`
	AuthenticationVector *AuthenticationVector `json:"authenticationVector,omitempty" yaml:"authenticationVector" bson:"authenticationVector" mapstructure:"AuthenticationVector"`
	Supi                 string                `json:"supi,omitempty" yaml:"supi" bson:"supi" mapstructure:"Supi"`
}

/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_authentication_vector.go */
type AuthenticationVector struct {
	AvType   AvType `json:"avType" yaml:"avType" bson:"avType" mapstructure:"AvType"`
	Rand     string `json:"rand" yaml:"rand" bson:"rand" mapstructure:"Rand"`
	Xres     string `json:"xres" yaml:"xres" bson:"xres" mapstructure:"Xres"`
	Autn     string `json:"autn" yaml:"autn" bson:"autn" mapstructure:"Autn"`
	CkPrime  string `json:"ckPrime" yaml:"ckPrime" bson:"ckPrime" mapstructure:"CkPrime"`
	IkPrime  string `json:"ikPrime" yaml:"ikPrime" bson:"ikPrime" mapstructure:"IkPrime"`
	XresStar string `json:"xresStar" yaml:"xresStar" bson:"xresStar" mapstructure:"XresStar"`
	Kausf    string `json:"kausf" yaml:"kausf" bson:"kausf" mapstructure:"Kausf"`
}

Auth5gAkaComfirmRequestProcedure

當使用者收到aka-challenge,計算出相應的Res後,就開始第二步向網路端確認自己的計算結果。網路端將對比使用者計算的Res和之前生成的XRes,確認是否鑑權成功。如果裝置使用的協議是5G-AKA協議,那麼相應的處理函式就是Auth5gAkaComfirmRequestProcedure,其簡化版程式碼如下:

// https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/processor/ue_authentication.go#L456
func (p *Processor) Auth5gAkaComfirmRequestProcedure(c *gin.Context, updateConfirmationData models.ConfirmationData,
	ConfirmationDataResponseID string,
) {
    var confirmDataRsp models.ConfirmationDataResponse
	ausfCurrentContext := ausf_context.GetAusfUeContext(currentSupi)

	// 對比收到的 RES* 和之前存起來的 XRES*
	if strings.EqualFold(updateConfirmationData.ResStar, ausfCurrentContext.XresStar) {
        // 鑑權成功,生成金鑰 KSeaf 
		ausfCurrentContext.AuthStatus = models.AuthResult_SUCCESS
		confirmDataRsp.AuthResult = models.AuthResult_SUCCESS
		success = true
		confirmDataRsp.Kseaf = ausfCurrentContext.Kseaf
	} else {
        // 鑑權失敗
		ausfCurrentContext.AuthStatus = models.AuthResult_FAILURE
		confirmDataRsp.AuthResult = models.AuthResult_FAILURE
		p.logConfirmFailureAndInformUDM(ConfirmationDataResponseID, models.AuthType__5_G_AKA, servingNetworkName,
			"5G AKA confirmation failed", ausfCurrentContext.UdmUeauUrl)
	}

	p.Consumer().SendAuthResultToUDM(currentSupi, models.AuthType__5_G_AKA, success, servingNetworkName,
		ausfCurrentContext.UdmUeauUrl)

	c.JSON(http.StatusOK, confirmDataRsp)
}
點選檢視更多openapi裡的型別
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_confirmation_data.go */
type ConfirmationData struct {
	ResStar string `json:"resStar" yaml:"resStar" bson:"resStar"`
}

/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_confirmation_data_response.go */
type ConfirmationDataResponse struct {
	AuthResult AuthResult `json:"authResult" yaml:"authResult" bson:"authResult"`
	Supi       string     `json:"supi,omitempty" yaml:"supi" bson:"supi"`
	Kseaf      string     `json:"kseaf,omitempty" yaml:"kseaf" bson:"kseaf"`
}

EapAuthComfirmRequestProcedure

可見這個函式的處理邏輯還是比較簡潔的,實際上未經簡化的完整函式也只有60行程式碼。而如果裝置使用的是EAP-AKA協議,那麼對應的EapAuthComfirmRequestProcedure函式就會相對來說更復雜點,其完整程式碼有173行,下面是簡化版程式碼。

https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/processor/ue_authentication.go#L36
func (p *Processor) EapAuthComfirmRequestProcedure(c *gin.Context, 
    updateEapSession models.EapSession, eapSessionID string) {
	var eapSession models.EapSession

	ausfCurrentContext := ausf_context.GetAusfUeContext(currentSupi)
	eapOK := true

    // 如果當前認證狀態已經是失敗,則返回401錯誤
    if ausfCurrentContext.AuthStatus == models.AuthResult_FAILURE {
		eapSession.AuthResult = models.AuthResult_FAILURE
		c.JSON(http.StatusUnauthorized, eapSession)
		return
	}

    // decodeEapAkaPrimePkt := 對 `updateEapSession.EapPayload` 的各種處理

	switch decodeEapAkaPrimePkt.Subtype {
		case ausf_context.AKA_CHALLENGE_SUBTYPE:
			XMAC := CalculateAtMAC(K_aut, decodeEapAkaPrimePkt.MACInput)
			MAC := decodeEapAkaPrimePkt.Attributes[ausf_context.AT_MAC_ATTRIBUTE].Value
			XRES := ausfCurrentContext.XRES
			RES := hex.EncodeToString(decodeEapAkaPrimePkt.Attributes[ausf_context.AT_RES_ATTRIBUTE].Value)

			if !bytes.Equal(MAC, XMAC) {
				eapOK = false
			} else if XRES == RES {
				// 鑑權成功,生成金鑰 KSeaf 
				eapSession.KSeaf = ausfCurrentContext.Kseaf
				eapSession.Supi = currentSupi
				eapSession.AuthResult = models.AuthResult_SUCCESS
				p.Consumer().SendAuthResultToUDM(eapSessionID, models.AuthType_EAP_AKA_PRIME,true, 
                    servingNetworkName, ausfCurrentContext.UdmUeauUrl)
				ausfCurrentContext.AuthStatus = models.AuthResult_SUCCESS
			} else {
				eapOK = false
			}
		default:
			ausfCurrentContext.AuthStatus = models.AuthResult_FAILURE
		}
	}

	if !eapOK {
		logger.AuthELog.Warnf("EAP-AKA' failure: %s", eapErrStr)
		p.Consumer().SendAuthResultToUDM(eapSessionID, models.AuthType_EAP_AKA_PRIME, false, servingNetworkName,
			ausfCurrentContext.UdmUeauUrl)
		ausfCurrentContext.AuthStatus = models.AuthResult_FAILURE
		eapSession.AuthResult = models.AuthResult_ONGOING
	} else if ausfCurrentContext.AuthStatus == models.AuthResult_FAILURE {
		p.Consumer().SendAuthResultToUDM(eapSessionID, models.AuthType_EAP_AKA_PRIME, false, servingNetworkName,
			ausfCurrentContext.UdmUeauUrl)
		eapSession.AuthResult = models.AuthResult_FAILURE
	}

	c.JSON(http.StatusOK, eapSession)
}

可以看到EAP協議中裝置傳送的請求和網路返回的回覆都是eapSession。網路拿到一個eapSession後要首先檢查一下ausfCurrentContext.AuthStatus是否已經被設定為failed,如果是的話直接返回鑑權失敗,因為這意味著裝置嘗試對一個aka-challenge進行多次求解。此後,將eapSession.EapPayload中的內容提取出來,分別會很對其MAC與XMAC,RES與XRES,如果都成功,則鑑權透過。

EAP-AKA之所以看起來比5G-AKA複雜,是因為它需要適應更廣泛的應用場景和網路環境,同時提供更多的安全和隱私保護選項。而5G-AKA作為專為5G設計的協議,雖然在安全性上進行了增強,但其設計目標更為集中,因此在實現上可能顯得更為精簡和高效。

點選檢視更多與EAP-AKA相關的型別
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_eap_session.go */
type EapSession struct {
	// contains an EAP packet
	EapPayload string                      `json:"eapPayload" yaml:"eapPayload" bson:"eapPayload"`
	KSeaf      string                      `json:"kSeaf,omitempty" yaml:"kSeaf" bson:"kSeaf"`
	Links      map[string]LinksValueSchema `json:"_links,omitempty" yaml:"_links" bson:"_links"`
	AuthResult AuthResult                  `json:"authResult,omitempty" yaml:"authResult" bson:"authResult"`
	Supi       string                      `json:"supi,omitempty" yaml:"supi" bson:"supi"`
}

// https://github.com/free5gc/ausf/blob/v1.2.3/internal/context/context.go#L57
type EapAkaPrimeAttribute struct {
	Type   uint8
	Length uint8
	Value  []byte
}

// https://github.com/free5gc/ausf/blob/v1.2.3/internal/context/context.go#L63
type EapAkaPrimePkt struct {
	Subtype    uint8
	Attributes map[uint8]EapAkaPrimeAttribute
	MACInput   []byte
}

// https://github.com/google/gopacket/blob/v1.1.19/layers/eap.go#L36
type EAP struct {
	BaseLayer
	Code     EAPCode
	Id       uint8
	Length   uint16
	Type     EAPType
	TypeData []byte
}

// https://github.com/google/gopacket/blob/v1.1.19/layers/base.go#L15
type BaseLayer struct {
	// Contents is the set of bytes that make up this layer.  IE: for an
	// Ethernet packet, this would be the set of bytes making up the
	// Ethernet frame.
	Contents []byte
	// Payload is the set of bytes contained by (but not part of) this
	// Layer.  Again, to take Ethernet as an example, this would be the
	// set of bytes encapsulated by the Ethernet protocol.
	Payload []byte
}

在本文中我們粗略瞭解了5G網路中裝置的認證鑑權過程,還深入研究了其原地阿瑪實現。

相關文章