Free5GC源码研究(4) - AUSF研究
本文研究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应当提供的服务:
可以看到,除了基本的设备鉴权服务(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的设计文档描绘了为其设计的软件架构(函数调用关系图):
整个图里面我们需要重点关注的函数是红框中的三个函数,它们才是实际解决鉴权问题的函数,其他都是简单处理或者套壳调用,而这三个函数都定义在internal/sbi/processor/ue_authentication.go这一个文件里。但在深入研究这三个函数以前,有必要先瞅一眼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里定义了各种类型,其中最值得留意的自然是AUSFContext
和AusfUeContext
,前者存储整个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网络中设备的认证鉴权过程,还深入研究了其源代码实现。