feat: add XeSIM CTE and RedPocket VoWiFi compatibility

This commit is contained in:
MengMengCode
2026-08-14 21:15:34 +08:00
parent edbd0ef4d1
commit 636d4e8a69
13 changed files with 559 additions and 29 deletions
+60
View File
@@ -0,0 +1,60 @@
package vowifi
import (
"fmt"
"strings"
)
const att310280EPDG = "epdg.epc.att.net"
// AssignedRoutePLMN returns a narrowly matched ePDG route PLMN without
// changing the subscription PLMN used for AKA identities. Some multi-profile
// and MVNO SIMs authenticate against their own HPLMN but use a host network's
// VoWiFi access gateway.
func AssignedRoutePLMN(iccid, imsi string) (string, string, bool) {
iccid = strings.TrimSpace(iccid)
imsi = strings.TrimSpace(imsi)
switch {
case strings.HasPrefix(iccid, "894416") && strings.HasPrefix(imsi, "204047"):
// XeSIM/Lebara: keep 204/04 for AKA and use Vodafone UK's ePDG.
return "234", "15", true
case strings.HasPrefix(iccid, "894430") && strings.HasPrefix(imsi, "23433"):
// CTExcel UK: keep 234/33 for AKA and use the EE UK ePDG used by
// the initial VoWiFi provisioning path.
return "234", "30", true
default:
return "", "", false
}
}
// IsATT310280 reports whether the live subscription is on AT&T's three-digit
// 310/280 PLMN. It is shared by SWu and IMS so the carrier exception cannot
// drift between protocol layers.
func IsATT310280(identity SIMIdentity) bool {
mcc := strings.TrimSpace(identity.HomeMCC)
mnc := strings.TrimLeft(strings.TrimSpace(identity.HomeMNC), "0")
imsi := strings.TrimSpace(identity.IMSI)
return mcc == "310" && mnc == "280" && strings.HasPrefix(imsi, "310280")
}
func applyAssignedCarrierRoute(identity SIMIdentity) SIMIdentity {
if strings.TrimSpace(identity.EPDG) != "" {
return identity
}
if routeMCC, routeMNC, ok := AssignedRoutePLMN(identity.ICCID, identity.IMSI); ok {
identity.EPDG = standardEPDGHostname(routeMCC, routeMNC)
}
return identity
}
func standardEPDGHostname(mcc, mnc string) string {
mnc = strings.TrimSpace(mnc)
for len(mnc) < 3 {
mnc = "0" + mnc
}
return fmt.Sprintf(
"epdg.epc.mnc%s.mcc%s.pub.3gppnetwork.org",
mnc,
strings.TrimSpace(mcc),
)
}
+56
View File
@@ -0,0 +1,56 @@
package vowifi
import "testing"
func TestAssignedRoutePLMNUsesNarrowCardAndSubscriptionMatches(t *testing.T) {
tests := []struct {
name string
iccid string
imsi string
wantMCC string
wantMNC string
wantAssigned bool
}{
{name: "XeSIM Lebara route", iccid: "89441600001001576265", imsi: "204047666157626", wantMCC: "234", wantMNC: "15", wantAssigned: true},
{name: "CTExcel initial route", iccid: "8944303773524055208", imsi: "234336570712415", wantMCC: "234", wantMNC: "30", wantAssigned: true},
{name: "XeSIM ICCID without matching subscription", iccid: "89441600001001576265", imsi: "204041666157626"},
{name: "similar ICCID must not match", iccid: "89441000001001576265", imsi: "204047666157626"},
{name: "generic EE SIM must not match CTExcel", iccid: "8944110000000000000", imsi: "234336570712415"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mcc, mnc, assigned := AssignedRoutePLMN(test.iccid, test.imsi)
if mcc != test.wantMCC || mnc != test.wantMNC || assigned != test.wantAssigned {
t.Fatalf("AssignedRoutePLMN() = %q/%q,%v, want %q/%q,%v", mcc, mnc, assigned, test.wantMCC, test.wantMNC, test.wantAssigned)
}
})
}
}
func TestApplyAssignedCarrierRoutePreservesAuthenticationPLMN(t *testing.T) {
identity := applyAssignedCarrierRoute(SIMIdentity{
ICCID: "8944303773524055208", IMSI: "234336570712415",
HomeMCC: "234", HomeMNC: "33",
})
if identity.HomeMCC != "234" || identity.HomeMNC != "33" {
t.Fatalf("authentication PLMN = %s/%s, want 234/33", identity.HomeMCC, identity.HomeMNC)
}
if identity.EPDG != "epdg.epc.mnc030.mcc234.pub.3gppnetwork.org" {
t.Fatalf("route ePDG = %q", identity.EPDG)
}
}
func TestIsATT310280RequiresMatchingPLMNAndIMSI(t *testing.T) {
if !IsATT310280(SIMIdentity{IMSI: "310280229187733", HomeMCC: "310", HomeMNC: "280"}) {
t.Fatal("AT&T 310/280 identity was not recognized")
}
for _, identity := range []SIMIdentity{
{IMSI: "310410229187733", HomeMCC: "310", HomeMNC: "280"},
{IMSI: "310280229187733", HomeMCC: "310", HomeMNC: "28"},
{IMSI: "310280229187733", HomeMCC: "311", HomeMNC: "280"},
} {
if IsATT310280(identity) {
t.Fatalf("unrelated identity matched AT&T 310/280: %#v", identity)
}
}
}
+135 -10
View File
@@ -97,9 +97,10 @@ type ec20RadioCheckpoint struct {
}
var (
_ SIMIdentityReader = (*EC20Adapter)(nil)
_ AKAProvider = (*EC20Adapter)(nil)
_ RadioController = (*EC20Adapter)(nil)
_ SIMIdentityReader = (*EC20Adapter)(nil)
_ AKAProvider = (*EC20Adapter)(nil)
_ PreferredAKAProvider = (*EC20Adapter)(nil)
_ RadioController = (*EC20Adapter)(nil)
)
func NewEC20Adapter(
@@ -172,6 +173,7 @@ func (adapter *EC20Adapter) ReadIdentity(
HomeMCC: homeMCC,
HomeMNC: homeMNC,
}
identity = applyAssignedCarrierRoute(identity)
adapter.mu.Lock()
adapter.bindings[iccid] = ec20SIMBinding{
deviceID: deviceID,
@@ -208,6 +210,11 @@ func (adapter *EC20Adapter) readHomePLMN(
iccid string,
imsi string,
) (string, string, error) {
// AT&T 310/280 is a three-digit MNC. Prefer the assigned subscription
// prefix when EF_AD is stale or ambiguous after a profile switch.
if strings.HasPrefix(strings.TrimSpace(imsi), "310280") {
return "310", "280", nil
}
mncLength, efErr := adapter.readExplicitMNCLength(ctx, deviceID)
if efErr == nil {
if len(imsi) < 3+mncLength {
@@ -238,9 +245,10 @@ func assignedHomePLMN(imsi string) (mcc, mnc string, ok bool) {
prefix string
mncLength int
}{
{prefix: "20404", mncLength: 2}, // Vodafone NL core; some Lebara subscriptions.
{prefix: "23415", mncLength: 2}, // Vodafone UK.
{prefix: "23487", mncLength: 2}, // Lebara Mobile UK.
{prefix: "20404", mncLength: 2}, // Vodafone NL core; some Lebara subscriptions.
{prefix: "23415", mncLength: 2}, // Vodafone UK.
{prefix: "23487", mncLength: 2}, // Lebara Mobile UK.
{prefix: "310280", mncLength: 3}, // AT&T / RedPocket GSMA.
}
for _, assignment := range assignments {
if strings.HasPrefix(imsi, assignment.prefix) {
@@ -391,11 +399,46 @@ func (adapter *EC20Adapter) Authenticate(
ctx context.Context,
identity SIMIdentity,
challenge AKAChallenge,
) (AKAResult, error) {
return adapter.authenticateWithApplication(ctx, identity, challenge, "")
}
func (adapter *EC20Adapter) AuthenticateWithPreference(
ctx context.Context,
identity SIMIdentity,
challenge AKAChallenge,
preference string,
) (AKAResult, error) {
return adapter.authenticateWithApplication(ctx, identity, challenge, preference)
}
func (adapter *EC20Adapter) authenticateWithApplication(
ctx context.Context,
identity SIMIdentity,
challenge AKAChallenge,
preference string,
) (AKAResult, error) {
binding, err := adapter.bindingFor(identity)
if err != nil {
return AKAResult{}, err
}
if strings.EqualFold(strings.TrimSpace(preference), "isim_strict") && binding.application != "ISIM" {
aid, application, err := adapter.discoverPreferredAKAApplication(
ctx,
binding.deviceID,
isimAIDPrefix,
"ISIM",
)
if err != nil {
return AKAResult{}, err
}
binding.aid = aid
binding.application = application
binding.basicChannel = false
adapter.mu.Lock()
adapter.bindings[binding.iccid] = binding
adapter.mu.Unlock()
}
if binding.aid == "" {
if _, err := adapter.CheckReady(ctx, identity); err != nil {
return AKAResult{}, err
@@ -405,6 +448,14 @@ func (adapter *EC20Adapter) Authenticate(
return AKAResult{}, err
}
}
if strings.EqualFold(strings.TrimSpace(preference), "isim_strict") && binding.application != "ISIM" {
return AKAResult{}, fmt.Errorf(
"%w: ISIM strict requested, selected %s (%s)",
ErrEC20ApplicationAbsent,
binding.application,
binding.aid,
)
}
if err := adapter.verifyLiveICCID(ctx, binding); err != nil {
return AKAResult{}, err
}
@@ -907,6 +958,28 @@ func (adapter *EC20Adapter) discoverAKAApplication(
return usimAIDPrefix, "USIM", nil
}
func (adapter *EC20Adapter) discoverPreferredAKAApplication(
ctx context.Context,
deviceID string,
aidPrefix string,
application string,
) (string, string, error) {
response, err := adapter.execute(ctx, deviceID, "AT+CUAD")
if err == nil {
data, parseErr := parseCUADData(response)
if parseErr == nil {
for _, candidate := range collectApplicationAIDs(data) {
if strings.HasPrefix(candidate, aidPrefix) {
return candidate, application, nil
}
}
}
}
// AT+CUAD is optional. Returning the standard AID prefix still lets CCHO
// perform the authoritative application probe on older EC20 firmware.
return aidPrefix, application, nil
}
func (adapter *EC20Adapter) openLogicalChannel(
ctx context.Context,
deviceID string,
@@ -1146,12 +1219,27 @@ func parseCRSMData(response modem.Response) ([]byte, error) {
}
func parseCUADData(response modem.Response) ([]byte, error) {
fields := parseCSV(valueAfterATPrefix(response, "+CUAD:"))
if len(fields) == 0 {
// EC20 firmware may split the BER-TLV stream across adjacent quoted chunks
// and continuation lines. Concatenating every hex fragment prevents an ISIM
// AID after a USIM entry from being silently discarded.
var encoded strings.Builder
collect := false
for _, line := range response.Lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(strings.ToUpper(line), "+CUAD:") {
collect = true
line = strings.TrimSpace(line[len("+CUAD:"):])
} else if !collect {
continue
}
for _, fragment := range quotedHexFragments(line) {
encoded.WriteString(fragment)
}
}
if encoded.Len() == 0 {
return nil, errors.New("CUAD response has no data")
}
value := fields[len(fields)-1]
data, err := hex.DecodeString(strings.Trim(value, `"`))
data, err := hex.DecodeString(encoded.String())
if err != nil || len(data) == 0 {
return nil, errors.New("CUAD response data is invalid")
}
@@ -1161,11 +1249,48 @@ func parseCUADData(response modem.Response) ([]byte, error) {
return data, nil
}
func quotedHexFragments(line string) []string {
var fragments []string
for {
start := strings.IndexByte(line, '"')
if start < 0 {
break
}
line = line[start+1:]
end := strings.IndexByte(line, '"')
if end < 0 {
break
}
fragment := strings.ToUpper(strings.TrimSpace(line[:end]))
line = line[end+1:]
if fragment == "" || len(fragment)%2 != 0 {
continue
}
valid := true
for _, character := range fragment {
if (character < '0' || character > '9') && (character < 'A' || character > 'F') {
valid = false
break
}
}
if valid {
fragments = append(fragments, fragment)
}
}
return fragments
}
func collectApplicationAIDs(data []byte) []string {
var result []string
var walk func([]byte)
walk = func(value []byte) {
for len(value) > 0 {
for len(value) > 0 && value[0] == 0xff {
value = value[1:]
}
if len(value) == 0 {
return
}
tag, constructed, body, consumed, err := decodeBERTLV(value)
if err != nil || consumed == 0 {
return
+94
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"testing"
@@ -421,6 +422,7 @@ func TestAssignedHomePLMNIncludesLebaraUKCores(t *testing.T) {
"204040123456789": "204/04",
"234150123456789": "234/15",
"234870123456789": "234/87",
"310280229187733": "310/280",
}
for imsi, want := range tests {
mcc, mnc, ok := assignedHomePLMN(imsi)
@@ -430,6 +432,23 @@ func TestAssignedHomePLMNIncludesLebaraUKCores(t *testing.T) {
}
}
func TestEC20AdapterTreatsATT310280AsThreeDigitMNC(t *testing.T) {
t.Parallel()
transcript := &ec20Transcript{t: t, steps: identityTranscriptStepsWithoutEFAD("310280229187733")}
adapter, err := NewEC20Adapter(transcript, EC20AdapterOptions{})
if err != nil {
t.Fatal(err)
}
identity, err := adapter.ReadIdentity(context.Background(), "ec20-1")
if err != nil {
t.Fatalf("ReadIdentity: %v", err)
}
if identity.HomeMCC != "310" || identity.HomeMNC != "280" {
t.Fatalf("home PLMN = %s/%s, want 310/280", identity.HomeMCC, identity.HomeMNC)
}
transcript.assertDone()
}
func TestEC20AdapterRadioTransactionRestoresCFUNAndPDPContexts(
t *testing.T,
) {
@@ -598,3 +617,78 @@ func synchronizationFailureUSIMResponse() []byte {
raw = append(raw, auts...)
return append(raw, 0x90, 0x00)
}
func TestCollectApplicationAIDsSkipsCUADPadding(t *testing.T) {
t.Parallel()
response := modem.Response{Lines: []string{
`+CUAD: "61184F10A0000000871002FFFFFFFF890302000050045553494DFFFFFFFFFFFFFFFFFFFFFFFF""61184F10A0000000871004FFFFFFFF890302000050044953494DFFFFFFFFFFFFFFFFFFFFFFFF"`,
`"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"`,
}}
data, err := parseCUADData(response)
if err != nil {
t.Fatal(err)
}
aids := collectApplicationAIDs(data)
want := []string{
"A0000000871002FFFFFFFF8903020000",
"A0000000871004FFFFFFFF8903020000",
}
if !reflect.DeepEqual(aids, want) {
t.Fatalf("AIDs = %v, want %v", aids, want)
}
}
func TestEC20AdapterISIMStrictUsesCUADFullAID(t *testing.T) {
var challenge AKAChallenge
for index := range challenge.RAND {
challenge.RAND[index] = byte(index)
challenge.AUTN[index] = byte(0xf0 + index)
}
authAPDU := buildUSIMAuthenticateAPDU(challenge)
authCommand := fmt.Sprintf(
`AT+CGLA=1,%d,"%s"`,
len(authAPDU)*2,
strings.ToUpper(hex.EncodeToString(authAPDU)),
)
encodedResponse := strings.ToUpper(hex.EncodeToString(successfulUSIMResponse()))
fullISIM := "A0000000871004FFFFFFFF8903020000"
cuad := `61184F10A0000000871002FFFFFFFF890302000050045553494D61184F10A0000000871004FFFFFFFF890302000050044953494D`
transcript := &ec20Transcript{
t: t,
steps: []ec20TranscriptStep{
{command: "AT+CPIN?", lines: []string{"+CPIN: READY"}},
{command: "AT+CIMI", lines: []string{"310280229187733"}},
{command: "AT+CCID", lines: []string{"+CCID: 89012804332291663965"}},
{command: "AT+CGSN", lines: []string{"863212060022487"}},
{command: "AT+CUAD", lines: []string{`+CUAD: "` + cuad + `"`}},
{command: "AT+CCID", lines: []string{"+CCID: 89012804332291663965"}},
{command: `AT+CCHO="` + fullISIM + `"`, lines: []string{"+CCHO: 1"}},
{
command: authCommand,
sensitive: true,
lines: []string{fmt.Sprintf(
`+CGLA: %d,"%s"`,
len(encodedResponse),
encodedResponse,
)},
},
{command: "AT+CCHC=1"},
},
}
adapter, err := NewEC20Adapter(transcript, EC20AdapterOptions{})
if err != nil {
t.Fatal(err)
}
identity, err := adapter.ReadIdentity(context.Background(), "ec20-1")
if err != nil {
t.Fatalf("ReadIdentity: %v", err)
}
result, err := adapter.AuthenticateWithPreference(context.Background(), identity, challenge, "isim_strict")
if err != nil {
t.Fatalf("AuthenticateWithPreference: %v", err)
}
if !bytes.Equal(result.RES, []byte{1, 2, 3, 4, 5, 6, 7, 8}) {
t.Fatalf("RES = %x", result.RES)
}
transcript.assertDone()
}
+12 -2
View File
@@ -168,6 +168,7 @@ func authenticateAKA(
provider vowifi.AKAProvider,
identity vowifi.SIMIdentity,
challenge digestChallenge,
preference string,
) (akaMaterial, error) {
nonce, err := decodeAKANonce(challenge.Nonce)
if err != nil {
@@ -178,9 +179,18 @@ func authenticateAKA(
var akaChallenge vowifi.AKAChallenge
copy(akaChallenge.RAND[:], nonce[:16])
copy(akaChallenge.AUTN[:], nonce[16:32])
result, err := provider.Authenticate(ctx, identity, akaChallenge)
var result vowifi.AKAResult
if preferred, ok := provider.(vowifi.PreferredAKAProvider); ok && strings.TrimSpace(preference) != "" {
result, err = preferred.AuthenticateWithPreference(ctx, identity, akaChallenge, preference)
} else {
result, err = provider.Authenticate(ctx, identity, akaChallenge)
}
if err != nil {
return akaMaterial{}, fmt.Errorf("ims: USIM AKA authentication failed: %w", err)
application := "USIM"
if strings.EqualFold(strings.TrimSpace(preference), "isim_strict") {
application = "ISIM"
}
return akaMaterial{}, fmt.Errorf("ims: %s AKA authentication failed: %w", application, err)
}
if result.SynchronizationFailure || len(result.AUTS) > 0 {
if !result.SynchronizationFailure || len(result.AUTS) != 14 {
+37
View File
@@ -16,6 +16,21 @@ type recordingAKA struct {
challenges []vowifi.AKAChallenge
}
type recordingPreferredAKA struct {
recordingAKA
preference string
}
func (aka *recordingPreferredAKA) AuthenticateWithPreference(
ctx context.Context,
identity vowifi.SIMIdentity,
challenge vowifi.AKAChallenge,
preference string,
) (vowifi.AKAResult, error) {
aka.preference = preference
return aka.Authenticate(ctx, identity, challenge)
}
func (aka *recordingAKA) CheckReady(context.Context, vowifi.SIMIdentity) (vowifi.AKAEvidence, error) {
return vowifi.AKAEvidence{Ready: true, Application: "usim"}, nil
}
@@ -60,6 +75,7 @@ func TestAuthenticateAKAMapsNonceToTypedChallenge(t *testing.T) {
aka,
vowifi.SIMIdentity{IMSI: "001010123456789"},
digestChallenge{Nonce: base64.StdEncoding.EncodeToString(nonceBytes)},
"",
)
if err != nil {
t.Fatalf("authenticateAKA() error = %v", err)
@@ -93,6 +109,7 @@ func TestAuthenticateAKAReturnsSynchronizationEvidence(t *testing.T) {
aka,
vowifi.SIMIdentity{},
digestChallenge{Nonce: nonce},
"",
)
if err != nil {
t.Fatalf("authenticateAKA() error = %v", err)
@@ -102,6 +119,26 @@ func TestAuthenticateAKAReturnsSynchronizationEvidence(t *testing.T) {
}
}
func TestAuthenticateAKAUsesPreferredApplicationWhenSupported(t *testing.T) {
nonce := base64.StdEncoding.EncodeToString(make([]byte, 32))
aka := &recordingPreferredAKA{recordingAKA: recordingAKA{
result: vowifi.AKAResult{RES: []byte{1, 2, 3, 4}},
}}
_, err := authenticateAKA(
context.Background(),
aka,
vowifi.SIMIdentity{IMSI: "310280229187733"},
digestChallenge{Nonce: nonce},
"isim_strict",
)
if err != nil {
t.Fatalf("authenticateAKA() error = %v", err)
}
if aka.preference != "isim_strict" {
t.Fatalf("preference = %q, want isim_strict", aka.preference)
}
}
func TestBuildDigestAuthorizationCarriesAUTSWithEmptyResponse(t *testing.T) {
authorization := buildDigestAuthorization(
digestChallenge{
+56 -5
View File
@@ -271,13 +271,22 @@ func deriveIdentities(identity vowifi.SIMIdentity, config Config) (identitySet,
mnc = "0" + mnc
}
domain := fmt.Sprintf("ims.mnc%s.mcc%s.3gppnetwork.org", mnc, mcc)
privateDomain := domain
publicDomain := domain
if vowifi.IsATT310280(identity) {
// AT&T provisions the IMPI and IMPU in its ISIM domains rather than
// the generic 3GPP PLMN IMS domain.
domain = "one.att.net"
privateDomain = "private.att.net"
publicDomain = "one.att.net"
}
privateIdentity := config.PrivateIdentity
if privateIdentity == "" {
privateIdentity = imsi + "@" + domain
privateIdentity = imsi + "@" + privateDomain
}
publicIdentity := config.PublicIdentity
if publicIdentity == "" {
publicIdentity = "sip:" + imsi + "@" + domain
publicIdentity = "sip:" + imsi + "@" + publicDomain
}
if strings.ContainsAny(privateIdentity+publicIdentity, "\r\n") ||
!strings.Contains(privateIdentity, "@") ||
@@ -536,6 +545,9 @@ func newSession(
}
protectedClientPort := provider.config.ProtectedClientPort
protectedServerPort := provider.config.ProtectedServerPort
if vowifi.IsATT310280(request.Identity) && protectedServerPort == 0 {
protectedServerPort = 6000
}
if securityEncryptionForIdentity(request.Identity) == "null" {
if protectedClientPort == 0 {
protectedClientPort = 5062
@@ -554,6 +566,10 @@ func newSession(
return nil, err
}
proposal.encryption = securityEncryptionForIdentity(request.Identity)
if vowifi.IsATT310280(request.Identity) {
proposal.integrityAlgorithms = []string{"hmac-sha-1-96"}
proposal.encryptionAlgorithmsList = []string{"aes-cbc"}
}
session.securityProposal = proposal
protectedTCP, err := net.ListenTCP(
"tcp",
@@ -732,7 +748,11 @@ func (session *Session) register(ctx context.Context, expires int) (*sipResponse
if err != nil {
return nil, err
}
material, err := authenticateAKA(ctx, session.provider.aka, session.request.Identity, challenge)
preference := ""
if vowifi.IsATT310280(session.request.Identity) {
preference = "isim_strict"
}
material, err := authenticateAKA(ctx, session.provider.aka, session.request.Identity, challenge, preference)
if err != nil {
return nil, err
}
@@ -792,6 +812,10 @@ func (session *Session) buildRegister(
authorizationHeader string,
authorization string,
) ([]byte, error) {
att310280 := vowifi.IsATT310280(session.request.Identity)
if att310280 {
expires = 18400
}
branch, err := randomHex(12)
if err != nil {
return nil, err
@@ -810,6 +834,17 @@ func (session *Session) buildRegister(
session.instanceID,
"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel",
)
if att310280 {
contact = fmt.Sprintf(
`<sip:%s@%s;transport=%s>;+g.3gpp.accesstype="wlan1";audio;+g.3gpp.smsip;`+
`+g.3gpp.icsi-ref="%s";+sip.instance="<%s>"`,
session.identity.user,
contactAddress,
session.transport,
"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel",
session.instanceID,
)
}
o2Germany := usesO2GermanyIMSProfile(session.request.Identity)
supported := "path, gruu"
allow := "REGISTER, INVITE, ACK, CANCEL, BYE, OPTIONS"
@@ -820,6 +855,13 @@ func (session *Session) buildRegister(
supported = "path, gruu, outbound, sec-agree, 100rel, timer"
allow = "INVITE, ACK, CANCEL, BYE, PRACK, UPDATE, INFO, MESSAGE, OPTIONS"
}
if att310280 {
supported = "path,sec-agree,gruu"
}
userAgent := strings.TrimSpace(session.provider.config.UserAgent)
if att310280 && (userAgent == "" || userAgent == "vocat/1") {
userAgent = "SimAdmin VoWiFi"
}
lines := []string{
"REGISTER " + requestURI + " SIP/2.0",
fmt.Sprintf("Via: SIP/2.0/%s %s;branch=z9hG4bK%s;rport", transportUpper, local, branch),
@@ -833,14 +875,23 @@ func (session *Session) buildRegister(
fmt.Sprintf("Expires: %d", expires),
"Supported: " + supported,
"Allow: " + allow,
"User-Agent: " + session.provider.config.UserAgent,
"User-Agent: " + userAgent,
}
if o2Germany {
lines = append(lines, "P-Preferred-Identity: <"+session.identity.public+">")
} else if att310280 {
lines = append(lines,
"P-Preferred-Identity: <"+session.identity.public+">",
`P-Visited-Network-ID: "one.att.net"`,
"P-Access-Network-Info: IEEE-802.11;i-wlan-node-id=000000000000;network-provided",
"Cellular-Network-Info: 3GPP-E-UTRAN-FDD;utran-cell-id-3gpp=3102800000000;cell-info-age=0",
"Accept-Contact: *;+g.3gpp.smsip",
`Accept-Contact: *;+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel"`,
)
}
if session.securityOffered() {
lines = append(lines,
"Security-Client: "+session.securityProposal.headerValue(),
"Security-Client: "+session.securityClientValue(),
"Require: sec-agree",
"Proxy-Require: sec-agree",
)
+69
View File
@@ -442,6 +442,75 @@ func TestO2GermanyInitialRegisterMatchesSupportedIMSProfile(t *testing.T) {
}
}
func TestATT310280DeriveIdentitiesUsesISIMDomains(t *testing.T) {
identities, err := deriveIdentities(vowifi.SIMIdentity{
IMSI: "310280229187733", HomeMCC: "310", HomeMNC: "280",
}, Config{})
if err != nil {
t.Fatalf("deriveIdentities() error = %v", err)
}
if identities.domain != "one.att.net" ||
identities.private != "[email protected]" ||
identities.public != "sip:[email protected]" {
t.Fatalf("AT&T identities = %#v", identities)
}
}
func TestATT310280InitialRegisterMatchesProvisionedProfile(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
identity := vowifi.SIMIdentity{
IMSI: "310280229187733", HomeMCC: "310", HomeMNC: "280",
}
identities, err := deriveIdentities(identity, Config{})
if err != nil {
t.Fatal(err)
}
session := &Session{
provider: &Provider{config: Config{SecurityMode: SecurityRequired, UserAgent: "vocat/1"}},
request: vowifi.IMSRequest{Identity: identity},
identity: identities,
endpoint: pcscfEndpoint{host: "pcscf.example", port: 5060},
transport: "tcp",
conn: client,
callID: "att-test",
fromTag: "tag",
instanceID: "urn:uuid:test",
securityProposal: securityProposal{
spiClient: 1546543, spiServer: 1546542,
portClient: 32773, portServer: 6000,
integrityAlgorithms: []string{"hmac-sha-1-96"},
encryptionAlgorithmsList: []string{"aes-cbc"},
},
}
packet, err := session.buildRegister(1, 3600, "", "")
if err != nil {
t.Fatalf("buildRegister() error = %v", err)
}
request := string(packet)
for _, want := range []string{
"REGISTER sip:one.att.net SIP/2.0",
"Expires: 18400",
"Supported: path,sec-agree,gruu",
"User-Agent: SimAdmin VoWiFi",
`+g.3gpp.accesstype="wlan1";audio;+g.3gpp.smsip`,
"P-Preferred-Identity: <sip:[email protected]>",
`P-Visited-Network-ID: "one.att.net"`,
"P-Access-Network-Info: IEEE-802.11;i-wlan-node-id=000000000000;network-provided",
"Cellular-Network-Info: 3GPP-E-UTRAN-FDD;utran-cell-id-3gpp=3102800000000;cell-info-age=0",
"Accept-Contact: *;+g.3gpp.smsip",
"Security-Client: ipsec-3gpp; alg=hmac-sha-1-96; ealg=aes-cbc; prot=esp; mod=trans; spi-c=1546543; spi-s=1546542; port-c=32773; port-s=6000",
`username="[email protected]"`,
`uri="sip:one.att.net"`,
} {
if !strings.Contains(request, want) {
t.Fatalf("AT&T REGISTER omits %q:\n%s", want, request)
}
}
}
func serveRefreshFailure(listener *net.UDPConn, nonce string) error {
var callID string
for step := 0; step < 3; step++ {
+15
View File
@@ -12,6 +12,8 @@ import (
"sort"
"strconv"
"strings"
"vocat/internal/vowifi"
)
type SecurityMode string
@@ -143,6 +145,19 @@ func (proposal securityProposal) headerValue() string {
return strings.Join(values, ", ")
}
func (session *Session) securityClientValue() string {
if vowifi.IsATT310280(session.request.Identity) {
return fmt.Sprintf(
"ipsec-3gpp; alg=hmac-sha-1-96; ealg=aes-cbc; prot=esp; mod=trans; spi-c=%d; spi-s=%d; port-c=%d; port-s=%d",
session.securityProposal.spiClient,
session.securityProposal.spiServer,
session.securityProposal.portClient,
session.securityProposal.portServer,
)
}
return session.securityProposal.headerValue()
}
func (proposal securityProposal) encryptionAlgorithm() string {
if strings.EqualFold(strings.TrimSpace(proposal.encryption), "null") {
return "null"
+4 -9
View File
@@ -645,18 +645,13 @@ func DeriveEPDG(identity SIMIdentity) (string, error) {
}
return strings.ToLower(configured), nil
}
if IsATT310280(identity) {
return att310280EPDG, nil
}
if err := identity.validate(); err != nil {
return "", err
}
mnc := strings.TrimSpace(identity.HomeMNC)
for len(mnc) < 3 {
mnc = "0" + mnc
}
return fmt.Sprintf(
"epdg.epc.mnc%s.mcc%s.pub.3gppnetwork.org",
mnc,
strings.TrimSpace(identity.HomeMCC),
), nil
return standardEPDGHostname(identity.HomeMCC, identity.HomeMNC), nil
}
func normalizeProxyRoute(route ProxyRoute) (ProxyRoute, error) {
+3 -3
View File
@@ -53,18 +53,18 @@ func (adapter *PCSCAdapter) ReadIdentity(ctx context.Context, deviceID string) (
mncLength := identity.MNCLength
if mncLength != 2 && mncLength != 3 {
if mcc, mnc, ok := assignedHomePLMN(identity.IMSI); ok {
return SIMIdentity{ICCID: identity.ICCID, IMSI: identity.IMSI, HomeMCC: mcc, HomeMNC: mnc, SMSC: identity.SMSC}, nil
return applyAssignedCarrierRoute(SIMIdentity{ICCID: identity.ICCID, IMSI: identity.IMSI, HomeMCC: mcc, HomeMNC: mnc, SMSC: identity.SMSC}), nil
}
return SIMIdentity{}, ErrEC20MNCUnavailable
}
if len(identity.IMSI) < 3+mncLength {
return SIMIdentity{}, errors.New("vocat: USB SIM IMSI is shorter than its EF_AD home PLMN")
}
return SIMIdentity{
return applyAssignedCarrierRoute(SIMIdentity{
ICCID: identity.ICCID, IMSI: identity.IMSI,
HomeMCC: identity.IMSI[:3], HomeMNC: identity.IMSI[3 : 3+mncLength],
SMSC: identity.SMSC,
}, nil
}), nil
}
func (adapter *PCSCAdapter) ReadSMSCenter(ctx context.Context, deviceID string) (string, error) {
+10
View File
@@ -125,6 +125,16 @@ func TestDeriveEPDGUsesExplicitPLMNAndNeverIMSIHeuristics(t *testing.T) {
},
want: "epdg.epc.mnc260.mcc310.pub.3gppnetwork.org",
},
{
name: "AT&T 310280 uses carrier endpoint",
identity: SIMIdentity{
ICCID: "89012804332291663965",
IMSI: "310280229187733",
HomeMCC: "310",
HomeMNC: "280",
},
want: "epdg.epc.att.net",
},
{
name: "explicit endpoint",
identity: SIMIdentity{
+8
View File
@@ -317,6 +317,14 @@ type AKAProvider interface {
Authenticate(context.Context, SIMIdentity, AKAChallenge) (AKAResult, error)
}
// PreferredAKAProvider optionally lets an AKA provider select a carrier-
// provisioned application such as ISIM. Providers that only expose USIM keep
// implementing AKAProvider unchanged.
type PreferredAKAProvider interface {
AKAProvider
AuthenticateWithPreference(context.Context, SIMIdentity, AKAChallenge, string) (AKAResult, error)
}
// RadioController owns the host/modem radio projection. EnterVoWiFiRFOff must
// not toggle the independent pure-airplane policy; Restore must return to the
// captured pre-transaction state.