mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-15 20:33:43 +08:00
Implement 3GPP Device Identity Handling in IKE
- Added support for DEVICE_IDENTITY request as per TS 24.302. - Implemented deviceIdentityRequested function to check for DEVICE_IDENTITY notifications. - Created deviceIdentityNotify function to construct DEVICE_IDENTITY notifications with IMEI/IMEISV. - Enhanced security proposal to accommodate O2 Germany's integrity-only ESP profile. - Updated tests to validate DEVICE_IDENTITY encoding and request handling. - Refactored security proposal to include fallback encryption and integrity algorithms. - Improved session management to ensure proper cleanup of IPSec resources after caller deadline. - Added comprehensive tests for security agreement parsing and algorithm support. FIX #11
This commit is contained in:
@@ -16,6 +16,7 @@ const (
|
||||
|
||||
configInternalIPv4Address = 1
|
||||
configInternalIPv4DNS = 3
|
||||
configApplicationVersion = 7
|
||||
configInternalIPv6Address = 8
|
||||
configInternalIPv6DNS = 10
|
||||
configPCSCFIPv4Address = 20
|
||||
@@ -186,6 +187,9 @@ func configurationRequest() payload {
|
||||
configInternalIPv6DNS,
|
||||
configPCSCFIPv4Address,
|
||||
configPCSCFIPv6Address,
|
||||
// Android's IKE library always appends APPLICATION_VERSION to the
|
||||
// initial configuration request, even when the value is empty.
|
||||
configApplicationVersion,
|
||||
}
|
||||
body := []byte{configRequest, 0, 0, 0}
|
||||
for _, attribute := range attributes {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package ike
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// deviceIdentityRequested reports the 3GPP DEVICE_IDENTITY request defined by
|
||||
// TS 24.302. Android remembers this request and answers it in a later EAP
|
||||
// IKE_AUTH request, but only after authenticating the ePDG.
|
||||
func deviceIdentityRequested(payloads []payload) (bool, error) {
|
||||
for _, item := range payloadsOfType(payloads, payloadNotify) {
|
||||
kind, _, err := parseNotify(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if kind == notifyDeviceIdentity {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func deviceIdentityNotify(identity string) (payload, error) {
|
||||
identity = strings.TrimSpace(identity)
|
||||
if (len(identity) != 15 && len(identity) != 16) || !decimalDigits(identity) {
|
||||
return payload{}, errors.New("ike: device identity must contain 15 or 16 digits")
|
||||
}
|
||||
identityType := byte(1) // IMEI
|
||||
if len(identity) == 16 {
|
||||
identityType = 2 // IMEISV
|
||||
}
|
||||
data := make([]byte, 11)
|
||||
// TS 24.302 Figure 8.2.9.2: this inner length excludes its own two
|
||||
// octets, and is therefore 9 for an IMEI/IMEISV value.
|
||||
binary.BigEndian.PutUint16(data[:2], 9)
|
||||
data[2] = identityType
|
||||
for index := 0; index < 8; index++ {
|
||||
low := identity[index*2] - '0'
|
||||
high := byte(0x0f)
|
||||
if index*2+1 < len(identity) {
|
||||
high = identity[index*2+1] - '0'
|
||||
}
|
||||
data[index+3] = high<<4 | low
|
||||
}
|
||||
return makeNotify(notifyDeviceIdentity, data), nil
|
||||
}
|
||||
|
||||
func decimalDigits(value string) bool {
|
||||
for _, digit := range value {
|
||||
if digit < '0' || digit > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return value != ""
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ike
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeviceIdentityNotifyMatchesAndroidEncoding(t *testing.T) {
|
||||
item, err := deviceIdentityNotify("123456789012345")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kind, data, err := parseNotify(item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []byte{0, 9, 1, 0x21, 0x43, 0x65, 0x87, 0x09, 0x21, 0x43, 0xf5}
|
||||
if kind != notifyDeviceIdentity || !bytes.Equal(data, want) {
|
||||
t.Fatalf("DEVICE_IDENTITY = %d/%x, want %d/%x", kind, data, notifyDeviceIdentity, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceIdentityNotifyAcceptsIMEISV(t *testing.T) {
|
||||
item, err := deviceIdentityNotify("1234567890123456")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, data, _ := parseNotify(item)
|
||||
if data[2] != 2 || data[10] != 0x65 {
|
||||
t.Fatalf("IMEISV data = %x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceIdentityRequested(t *testing.T) {
|
||||
requested, err := deviceIdentityRequested([]payload{makeNotify(notifyDeviceIdentity, nil)})
|
||||
if err != nil || !requested {
|
||||
t.Fatalf("deviceIdentityRequested() = %v, %v", requested, err)
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
|
||||
group := uint16(dhMODP2048)
|
||||
legacyFirst := legacyIKEProfile(request.Identity.HomeMCC, request.Identity.HomeMNC)
|
||||
eapOnly := eapOnlyAuthentication(request.Identity.HomeMCC, request.Identity.HomeMNC)
|
||||
advertiseEAPOnly := advertiseEAPOnlyAuthentication(request.Identity.HomeMCC, request.Identity.HomeMNC)
|
||||
if legacyFirst {
|
||||
group = dhMODP1024
|
||||
}
|
||||
@@ -246,6 +246,23 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cleanupPendingIKE := true
|
||||
cleanupMessageID := uint32(2)
|
||||
defer func() {
|
||||
if cleanupPendingIKE {
|
||||
cleanupContext, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
_ = sendIKESADelete(
|
||||
cleanupContext,
|
||||
transport,
|
||||
ikeSuite,
|
||||
keys,
|
||||
initiatorSPI,
|
||||
responseHeader.ResponderSPI,
|
||||
cleanupMessageID,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
var childInboundSPIBytes [4]byte
|
||||
if err := fillNonzero(provider.config.Random, childInboundSPIBytes[:]); err != nil {
|
||||
@@ -260,7 +277,7 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
requestedIDr := payload{Type: payloadIDr, Body: append([]byte{2, 0, 0, 0}, []byte(provider.config.APN)...)}
|
||||
tsi := dualStackTrafficSelectors(payloadTSi)
|
||||
tsr := dualStackTrafficSelectors(payloadTSr)
|
||||
firstAuthPayloads := buildInitialEAPAuth(idi, requestedIDr, childOfferBody, tsi, tsr, eapOnly)
|
||||
firstAuthPayloads := buildInitialEAPAuth(idi, requestedIDr, childOfferBody, tsi, tsr, advertiseEAPOnly)
|
||||
authHeader := ikeHeader{
|
||||
InitiatorSPI: initiatorSPI,
|
||||
ResponderSPI: responseHeader.ResponderSPI,
|
||||
@@ -293,15 +310,19 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
initiatorNonce,
|
||||
ikeSuite,
|
||||
keys.SKpr,
|
||||
serverName,
|
||||
"", // Android Iwlan enables IKE_OPTION_ACCEPT_ANY_REMOTE_ID.
|
||||
serverName,
|
||||
provider.config.RootCAs,
|
||||
provider.config.ResponderPublicKey,
|
||||
eapOnly, // RFC 5998 EAP-only authentication defers responder AUTH.
|
||||
true, // Some ePDGs, including O2 Germany, implicitly defer AUTH without accepting the RFC 5998 notify.
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceIdentityPending, err := deviceIdentityRequested(authResponsePayloads)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messageID := uint32(1)
|
||||
currentPayloads := authResponsePayloads
|
||||
for round := 0; round < 10; round++ {
|
||||
@@ -320,16 +341,29 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
return nil, errors.New("ike: EAP state machine produced no response")
|
||||
}
|
||||
messageID++
|
||||
cleanupMessageID = messageID + 1
|
||||
requestPayloads := []payload{{Type: payloadEAP, Body: action.Response}}
|
||||
if deviceIdentityPending && responderAUTH == vowifi.ResponderAUTHVerified {
|
||||
deviceIdentity, identityErr := deviceIdentityNotify(request.Identity.IMEI)
|
||||
if identityErr == nil {
|
||||
requestPayloads = append(requestPayloads, deviceIdentity)
|
||||
}
|
||||
}
|
||||
eapRequest, err := encryptPayloads(ikeHeader{
|
||||
InitiatorSPI: initiatorSPI,
|
||||
ResponderSPI: responseHeader.ResponderSPI,
|
||||
Exchange: exchangeIKEAuth,
|
||||
Flags: flagInitiator,
|
||||
MessageID: messageID,
|
||||
}, []payload{{Type: payloadEAP, Body: action.Response}}, ikeSuite, keys.SKei, keys.SKai, provider.config.Random)
|
||||
}, requestPayloads, ikeSuite, keys.SKei, keys.SKai, provider.config.Random)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if requested, notifyErr := deviceIdentityRequested(currentPayloads); notifyErr != nil {
|
||||
return nil, notifyErr
|
||||
} else if requested {
|
||||
deviceIdentityPending = true
|
||||
}
|
||||
eapResponse, err := transport.RoundTrip(ctx, eapRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -359,6 +393,7 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
return nil, err
|
||||
}
|
||||
messageID++
|
||||
cleanupMessageID = messageID + 1
|
||||
finalRequest, err := encryptPayloads(ikeHeader{
|
||||
InitiatorSPI: initiatorSPI,
|
||||
ResponderSPI: responseHeader.ResponderSPI,
|
||||
@@ -394,7 +429,7 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
return nil, errors.New("ike: duplicate final responder IDr payload")
|
||||
}
|
||||
if len(finalIDs) == 1 {
|
||||
if err := validateFQDNIDr(finalIDs[0], provider.config.APN, "final APN"); err != nil {
|
||||
if err := validateFQDNIDr(finalIDs[0], "", "final responder"); err != nil {
|
||||
return nil, fmt.Errorf("ike: final APN IDr: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -473,9 +508,11 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
keys,
|
||||
initiatorSPI,
|
||||
responseHeader.ResponderSPI,
|
||||
messageID+1,
|
||||
natDetected,
|
||||
provider.config.KeepaliveInterval,
|
||||
)
|
||||
cleanupPendingIKE = false
|
||||
installed, err := provider.config.Installer.Install(ctx, ChildSAConfig{
|
||||
Name: name,
|
||||
OuterLocal: append(net.IP(nil), transport.LocalAddr().IP...),
|
||||
@@ -500,11 +537,11 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.TunnelReques
|
||||
Relay: relay,
|
||||
})
|
||||
if err != nil {
|
||||
_ = relay.Close()
|
||||
_ = relay.CloseWithDelete(ctx)
|
||||
return nil, fmt.Errorf("ike: install CHILD_SA: %w", err)
|
||||
}
|
||||
if installed == nil {
|
||||
_ = relay.Close()
|
||||
_ = relay.CloseWithDelete(ctx)
|
||||
return nil, errors.New("ike: CHILD_SA installer returned a nil handle")
|
||||
}
|
||||
dataplaneMode := "unknown"
|
||||
@@ -554,14 +591,19 @@ func legacyIKEProfile(mcc, mnc string) bool {
|
||||
return plmn == "23415" || plmn == "2044"
|
||||
}
|
||||
|
||||
func eapOnlyAuthentication(mcc, mnc string) bool {
|
||||
func advertiseEAPOnlyAuthentication(mcc, mnc string) bool {
|
||||
// Android exposes the ePDG authentication method as carrier policy rather
|
||||
// than unconditionally requesting RFC 5998 EAP-only authentication. O2
|
||||
// Germany's 262-03 ePDG rejects an EAP-only initial IKE_AUTH before it sends
|
||||
// an EAP-AKA identity or challenge. Use the certificate-authenticated EAP
|
||||
// flow for that PLMN while preserving the established behavior elsewhere.
|
||||
// Germany's 262-03 ePDG rejects an initial IKE_AUTH that explicitly carries
|
||||
// EAP_ONLY_AUTHENTICATION, but then implicitly defers responder AUTH when the
|
||||
// notify is omitted. Do not advertise RFC 5998 for that PLMN; the final
|
||||
// responder AUTH derived from the EAP-AKA MSK remains mandatory.
|
||||
return !o2GermanyIKECompatibility(mcc, mnc)
|
||||
}
|
||||
|
||||
func o2GermanyIKECompatibility(mcc, mnc string) bool {
|
||||
plmn := strings.TrimSpace(mcc) + strings.TrimLeft(strings.TrimSpace(mnc), "0")
|
||||
return plmn != "2623"
|
||||
return plmn == "2623"
|
||||
}
|
||||
|
||||
func buildInitialEAPAuth(
|
||||
@@ -572,10 +614,16 @@ func buildInitialEAPAuth(
|
||||
tsr payload,
|
||||
eapOnly bool,
|
||||
) []payload {
|
||||
// Match Android's IkeSessionStateMachine.buildIkeAuthReq ordering. Some
|
||||
// carrier ePDGs inspect this first encrypted exchange before starting EAP.
|
||||
payloads := []payload{idi, requestedIDr}
|
||||
if eapOnly {
|
||||
payloads = append(payloads, makeNotify(notifyEAPOnlyAuth, nil))
|
||||
}
|
||||
payloads = append(payloads,
|
||||
makeNotify(notifyMOBIKESupported, nil),
|
||||
makeNotify(notifyInitialContact, nil),
|
||||
)
|
||||
return append(payloads,
|
||||
payload{Type: payloadSA, Body: append([]byte(nil), childOfferBody...)},
|
||||
tsi,
|
||||
@@ -900,7 +948,7 @@ func (session *Session) Close(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
if relay != nil {
|
||||
if err := relay.Close(); err != nil {
|
||||
if err := relay.CloseWithDelete(ctx); err != nil {
|
||||
errs = append(errs, fmt.Errorf("close session relay: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type sessionRelay struct {
|
||||
keys ikeKeys
|
||||
spii [8]byte
|
||||
spir [8]byte
|
||||
deleteID uint32
|
||||
natt bool
|
||||
keepalive time.Duration
|
||||
|
||||
@@ -33,6 +34,7 @@ func newSessionRelay(
|
||||
keys ikeKeys,
|
||||
initiatorSPI [8]byte,
|
||||
responderSPI [8]byte,
|
||||
deleteMessageID uint32,
|
||||
natt bool,
|
||||
keepalive time.Duration,
|
||||
) *sessionRelay {
|
||||
@@ -46,6 +48,7 @@ func newSessionRelay(
|
||||
keys: keys,
|
||||
spii: initiatorSPI,
|
||||
spir: responderSPI,
|
||||
deleteID: deleteMessageID,
|
||||
natt: natt,
|
||||
keepalive: keepalive,
|
||||
ctx: ctx,
|
||||
@@ -214,6 +217,53 @@ func (relay *sessionRelay) Close() error {
|
||||
return errors.Join(relay.terminalErrorIfFailure(), transportErr)
|
||||
}
|
||||
|
||||
func (relay *sessionRelay) CloseWithDelete(ctx context.Context) error {
|
||||
deleteErr := relay.sendIKEDelete(ctx)
|
||||
return errors.Join(deleteErr, relay.Close())
|
||||
}
|
||||
|
||||
func (relay *sessionRelay) sendIKEDelete(ctx context.Context) error {
|
||||
return sendIKESADelete(ctx, relay.transport, relay.suite, relay.keys, relay.spii, relay.spir, relay.deleteID)
|
||||
}
|
||||
|
||||
func sendIKESADelete(
|
||||
ctx context.Context,
|
||||
transport datagramTransport,
|
||||
suite negotiatedSuite,
|
||||
keys ikeKeys,
|
||||
initiatorSPI [8]byte,
|
||||
responderSPI [8]byte,
|
||||
messageID uint32,
|
||||
) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
// Teardown is often called with the operation context already canceled.
|
||||
// Give the protocol-level release a short independent chance to leave.
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
request, err := encryptPayloads(ikeHeader{
|
||||
InitiatorSPI: initiatorSPI,
|
||||
ResponderSPI: responderSPI,
|
||||
Exchange: exchangeInformational,
|
||||
Flags: flagInitiator,
|
||||
MessageID: messageID,
|
||||
}, []payload{{
|
||||
Type: payloadDelete,
|
||||
Body: []byte{protocolIKE, 0, 0, 0},
|
||||
}}, suite, keys.SKei, keys.SKai, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ike: build IKE SA delete: %w", err)
|
||||
}
|
||||
if err := transport.SendSessionPacket(ctx, request, true); err != nil {
|
||||
return fmt.Errorf("ike: send IKE SA delete: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (relay *sessionRelay) terminalErrorIfFailure() error {
|
||||
relay.mu.Lock()
|
||||
defer relay.mu.Unlock()
|
||||
|
||||
@@ -119,6 +119,7 @@ func TestSessionRelayCloseInterruptsStuckTransportRead(t *testing.T) {
|
||||
ikeKeys{},
|
||||
[8]byte{1},
|
||||
[8]byte{2},
|
||||
9,
|
||||
true,
|
||||
time.Hour,
|
||||
)
|
||||
@@ -156,7 +157,7 @@ func TestSessionRelayDemuxesESPAndAnswersEncryptedDPD(t *testing.T) {
|
||||
}
|
||||
spii := [8]byte{1}
|
||||
spir := [8]byte{2}
|
||||
relay := newSessionRelay(transport, suite, keys, spii, spir, true, time.Hour)
|
||||
relay := newSessionRelay(transport, suite, keys, spii, spir, 9, true, time.Hour)
|
||||
defer relay.Close()
|
||||
|
||||
esp := []byte{0, 0, 0, 9, 0, 0, 0, 1, 0xaa}
|
||||
@@ -201,6 +202,47 @@ func TestSessionRelayDemuxesESPAndAnswersEncryptedDPD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionRelaySendsEncryptedIKESADelete(t *testing.T) {
|
||||
transport := newFakeSessionTransport()
|
||||
suite := legacyTestSuite()
|
||||
keys := ikeKeys{
|
||||
SKai: bytes.Repeat([]byte{0x11}, 20),
|
||||
SKar: bytes.Repeat([]byte{0x12}, 20),
|
||||
SKei: bytes.Repeat([]byte{0x13}, 16),
|
||||
SKer: bytes.Repeat([]byte{0x14}, 16),
|
||||
}
|
||||
spii := [8]byte{1}
|
||||
spir := [8]byte{2}
|
||||
relay := newSessionRelay(transport, suite, keys, spii, spir, 9, true, time.Hour)
|
||||
defer relay.Close()
|
||||
|
||||
if err := relay.sendIKEDelete(context.Background()); err != nil {
|
||||
t.Fatalf("sendIKEDelete() error = %v", err)
|
||||
}
|
||||
select {
|
||||
case sent := <-transport.sent:
|
||||
if !sent.ike {
|
||||
t.Fatal("IKE SA delete was sent as ESP")
|
||||
}
|
||||
header, payloads, err := decryptPayloads(sent.data, suite, keys.SKei, keys.SKai)
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt IKE SA delete: %v", err)
|
||||
}
|
||||
if header.Exchange != exchangeInformational || header.MessageID != 9 || header.Flags != flagInitiator {
|
||||
t.Fatalf("IKE SA delete header = %#v", header)
|
||||
}
|
||||
item, err := onePayload(payloads, payloadDelete)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(item.Body, []byte{protocolIKE, 0, 0, 0}) {
|
||||
t.Fatalf("IKE SA delete body = %x", item.Body)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("relay did not send IKE SA delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionRelaySendsNATKeepalive(t *testing.T) {
|
||||
transport := newFakeSessionTransport()
|
||||
relay := newSessionRelay(
|
||||
@@ -209,6 +251,7 @@ func TestSessionRelaySendsNATKeepalive(t *testing.T) {
|
||||
ikeKeys{},
|
||||
[8]byte{1},
|
||||
[8]byte{2},
|
||||
9,
|
||||
true,
|
||||
10*time.Millisecond,
|
||||
)
|
||||
@@ -233,6 +276,7 @@ func TestSessionRelayDropsDelayedIKEPacketFromPreviousSA(t *testing.T) {
|
||||
ikeKeys{},
|
||||
spii,
|
||||
spir,
|
||||
9,
|
||||
true,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
payloadAuth = 39
|
||||
payloadNonce = 40
|
||||
payloadNotify = 41
|
||||
payloadDelete = 42
|
||||
payloadTSi = 44
|
||||
payloadTSr = 45
|
||||
payloadEncrypted = 46
|
||||
@@ -54,11 +55,14 @@ const (
|
||||
dhMODP2048 = 14
|
||||
transformAttributeKeyLen = 14
|
||||
|
||||
notifyNATSource = 16388
|
||||
notifyNATDestination = 16389
|
||||
notifyEAPOnlyAuth = 16417
|
||||
notifyInvalidKE = 17
|
||||
notifyNoProposal = 14
|
||||
notifyInitialContact = 16384
|
||||
notifyMOBIKESupported = 16396
|
||||
notifyNATSource = 16388
|
||||
notifyNATDestination = 16389
|
||||
notifyEAPOnlyAuth = 16417
|
||||
notifyDeviceIdentity = 41101
|
||||
notifyInvalidKE = 17
|
||||
notifyNoProposal = 14
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,6 +3,7 @@ package ike
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"vocat/internal/vowifi"
|
||||
@@ -60,7 +61,7 @@ func TestInitialEAPOnlyAuthCarriesAPNIDrAndNotify(t *testing.T) {
|
||||
dualStackTrafficSelectors(payloadTSi),
|
||||
dualStackTrafficSelectors(payloadTSr),
|
||||
)
|
||||
if len(payloads) != 7 || payloads[0].Type != payloadIDi || payloads[1].Type != payloadIDr {
|
||||
if len(payloads) != 9 || payloads[0].Type != payloadIDi || payloads[1].Type != payloadIDr {
|
||||
t.Fatalf("initial auth payload order = %#v", payloads)
|
||||
}
|
||||
if got := string(payloads[1].Body[4:]); got != "ims" || payloads[1].Body[0] != 2 {
|
||||
@@ -68,11 +69,25 @@ func TestInitialEAPOnlyAuthCarriesAPNIDrAndNotify(t *testing.T) {
|
||||
}
|
||||
kind, data, err := parseNotify(payloads[2])
|
||||
if err != nil {
|
||||
t.Fatalf("parseNotify() error = %v", err)
|
||||
t.Fatalf("parseNotify(EAP_ONLY_AUTHENTICATION) error = %v", err)
|
||||
}
|
||||
if kind != notifyEAPOnlyAuth || len(data) != 0 {
|
||||
t.Fatalf("notify = %d/%x, want EAP_ONLY_AUTHENTICATION", kind, data)
|
||||
}
|
||||
kind, data, err = parseNotify(payloads[3])
|
||||
if err != nil {
|
||||
t.Fatalf("parseNotify() error = %v", err)
|
||||
}
|
||||
if kind != notifyMOBIKESupported || len(data) != 0 {
|
||||
t.Fatalf("notify = %d/%x, want MOBIKE_SUPPORTED", kind, data)
|
||||
}
|
||||
kind, data, err = parseNotify(payloads[4])
|
||||
if err != nil {
|
||||
t.Fatalf("parseNotify() error = %v", err)
|
||||
}
|
||||
if kind != notifyInitialContact || len(data) != 0 {
|
||||
t.Fatalf("notify = %d/%x, want INITIAL_CONTACT", kind, data)
|
||||
}
|
||||
for _, kind := range []uint8{payloadTSi, payloadTSr} {
|
||||
item, err := onePayload(payloads, kind)
|
||||
if err != nil {
|
||||
@@ -99,9 +114,11 @@ func TestInitialStandardEAPAuthOmitsEAPOnlyNotify(t *testing.T) {
|
||||
dualStackTrafficSelectors(payloadTSr),
|
||||
false,
|
||||
)
|
||||
if len(payloads) != 6 || payloads[0].Type != payloadIDi || payloads[1].Type != payloadIDr {
|
||||
if len(payloads) != 8 || payloads[0].Type != payloadIDi || payloads[1].Type != payloadIDr {
|
||||
t.Fatalf("initial standard EAP payload order = %#v", payloads)
|
||||
}
|
||||
initialContact := 0
|
||||
mobikeSupported := 0
|
||||
for _, item := range payloadsOfType(payloads, payloadNotify) {
|
||||
kind, _, err := parseNotify(item)
|
||||
if err != nil {
|
||||
@@ -110,16 +127,55 @@ func TestInitialStandardEAPAuthOmitsEAPOnlyNotify(t *testing.T) {
|
||||
if kind == notifyEAPOnlyAuth {
|
||||
t.Fatal("standard EAP initial request contains EAP_ONLY_AUTHENTICATION")
|
||||
}
|
||||
if kind == notifyInitialContact {
|
||||
initialContact++
|
||||
}
|
||||
if kind == notifyMOBIKESupported {
|
||||
mobikeSupported++
|
||||
}
|
||||
}
|
||||
if initialContact != 1 {
|
||||
t.Fatalf("standard EAP initial request INITIAL_CONTACT count = %d, want 1", initialContact)
|
||||
}
|
||||
if mobikeSupported != 1 {
|
||||
t.Fatalf("standard EAP initial request MOBIKE_SUPPORTED count = %d, want 1", mobikeSupported)
|
||||
}
|
||||
if payloads[2].Type != payloadNotify || payloads[3].Type != payloadNotify {
|
||||
t.Fatalf("standard EAP Android notify order = %#v", payloads[:4])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigurationRequestMatchesAndroidAttributes(t *testing.T) {
|
||||
cp := configurationRequest()
|
||||
if cp.Type != payloadCP || len(cp.Body) < 4 || cp.Body[0] != configRequest {
|
||||
t.Fatalf("configuration request = %#v", cp)
|
||||
}
|
||||
var attributes []uint16
|
||||
for offset := 4; offset < len(cp.Body); {
|
||||
if offset+4 > len(cp.Body) {
|
||||
t.Fatalf("truncated attribute at %d", offset)
|
||||
}
|
||||
kind := uint16(cp.Body[offset])<<8 | uint16(cp.Body[offset+1])
|
||||
length := int(cp.Body[offset+2])<<8 | int(cp.Body[offset+3])
|
||||
if offset+4+length > len(cp.Body) {
|
||||
t.Fatalf("attribute %d exceeds payload", kind)
|
||||
}
|
||||
attributes = append(attributes, kind)
|
||||
offset += 4 + length
|
||||
}
|
||||
want := []uint16{1, 8, 3, 10, 20, 21, configApplicationVersion}
|
||||
if !slices.Equal(attributes, want) {
|
||||
t.Fatalf("configuration attributes = %v, want %v", attributes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestO2GermanyUsesStandardEAPAuthentication(t *testing.T) {
|
||||
for _, mnc := range []string{"03", "003"} {
|
||||
if eapOnlyAuthentication("262", mnc) {
|
||||
if advertiseEAPOnlyAuthentication("262", mnc) {
|
||||
t.Fatalf("O2 Germany 262-%s unexpectedly uses EAP-only", mnc)
|
||||
}
|
||||
}
|
||||
if !eapOnlyAuthentication("262", "02") || !eapOnlyAuthentication("234", "15") {
|
||||
if !advertiseEAPOnlyAuthentication("262", "02") || !advertiseEAPOnlyAuthentication("234", "15") {
|
||||
t.Fatal("non-O2 PLMN lost the existing EAP-only policy")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,15 +534,26 @@ func newSession(
|
||||
refreshCancel()
|
||||
return nil, errors.New("ims: protected local IP address is unavailable")
|
||||
}
|
||||
protectedClientPort := provider.config.ProtectedClientPort
|
||||
protectedServerPort := provider.config.ProtectedServerPort
|
||||
if securityEncryptionForIdentity(request.Identity) == "null" {
|
||||
if protectedClientPort == 0 {
|
||||
protectedClientPort = 5062
|
||||
}
|
||||
if protectedServerPort == 0 {
|
||||
protectedServerPort = 5063
|
||||
}
|
||||
}
|
||||
proposal, err := newSecurityProposal(
|
||||
localIP,
|
||||
provider.config.ProtectedClientPort,
|
||||
provider.config.ProtectedServerPort,
|
||||
protectedClientPort,
|
||||
protectedServerPort,
|
||||
)
|
||||
if err != nil {
|
||||
refreshCancel()
|
||||
return nil, err
|
||||
}
|
||||
proposal.encryption = securityEncryptionForIdentity(request.Identity)
|
||||
session.securityProposal = proposal
|
||||
protectedTCP, err := net.ListenTCP(
|
||||
"tcp",
|
||||
@@ -567,6 +578,21 @@ func newSession(
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func securityEncryptionForIdentity(identity vowifi.SIMIdentity) string {
|
||||
if usesO2GermanyIMSProfile(identity) {
|
||||
// O2 Germany's P-CSCF advertises the 3GPP integrity-only ESP profile.
|
||||
// Proposing aes-cbc is rejected before the AKA challenge is issued.
|
||||
return "null"
|
||||
}
|
||||
return "aes-cbc"
|
||||
}
|
||||
|
||||
func usesO2GermanyIMSProfile(identity vowifi.SIMIdentity) bool {
|
||||
mcc := strings.TrimSpace(identity.HomeMCC)
|
||||
mnc := strings.TrimLeft(strings.TrimSpace(identity.HomeMNC), "0")
|
||||
return mcc+mnc == "2623"
|
||||
}
|
||||
|
||||
func (session *Session) abort() {
|
||||
session.refreshCancel()
|
||||
_ = session.conn.Close()
|
||||
@@ -635,6 +661,15 @@ func registrationRejectionError(response *sipResponse, phase string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
// P-Debug-Info is carrier-generated but can contain subscriber identifiers.
|
||||
// Surface only a fixed classification for the O2 security-agreement error;
|
||||
// never copy the raw header into logs or API responses.
|
||||
for _, value := range response.values("P-Debug-Info") {
|
||||
if strings.Contains(strings.ToLower(value), "no matched security item") {
|
||||
message += "; carrier detail: no matched IMS security item"
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrRegistrationRejected, message)
|
||||
}
|
||||
|
||||
@@ -775,6 +810,16 @@ func (session *Session) buildRegister(
|
||||
session.instanceID,
|
||||
"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel",
|
||||
)
|
||||
o2Germany := usesO2GermanyIMSProfile(session.request.Identity)
|
||||
supported := "path, gruu"
|
||||
allow := "REGISTER, INVITE, ACK, CANCEL, BYE, OPTIONS"
|
||||
if o2Germany {
|
||||
// Match the complete IMS capability set used by the previously working
|
||||
// VoHive client. O2 validates more of the initial UE security profile
|
||||
// than the other tested carriers do.
|
||||
supported = "path, gruu, outbound, sec-agree, 100rel, timer"
|
||||
allow = "INVITE, ACK, CANCEL, BYE, PRACK, UPDATE, INFO, MESSAGE, OPTIONS"
|
||||
}
|
||||
lines := []string{
|
||||
"REGISTER " + requestURI + " SIP/2.0",
|
||||
fmt.Sprintf("Via: SIP/2.0/%s %s;branch=z9hG4bK%s;rport", transportUpper, local, branch),
|
||||
@@ -786,13 +831,15 @@ func (session *Session) buildRegister(
|
||||
fmt.Sprintf("CSeq: %d REGISTER", cseq),
|
||||
"Contact: " + contact,
|
||||
fmt.Sprintf("Expires: %d", expires),
|
||||
"Supported: path, gruu",
|
||||
"Allow: REGISTER, INVITE, ACK, CANCEL, BYE, OPTIONS",
|
||||
"Supported: " + supported,
|
||||
"Allow: " + allow,
|
||||
"User-Agent: " + session.provider.config.UserAgent,
|
||||
}
|
||||
if o2Germany {
|
||||
lines = append(lines, "P-Preferred-Identity: <"+session.identity.public+">")
|
||||
}
|
||||
if session.securityOffered() {
|
||||
lines = append(
|
||||
lines,
|
||||
lines = append(lines,
|
||||
"Security-Client: "+session.securityProposal.headerValue(),
|
||||
"Require: sec-agree",
|
||||
"Proxy-Require: sec-agree",
|
||||
@@ -1194,7 +1241,14 @@ func (session *Session) Close(ctx context.Context) error {
|
||||
session.closeInboundConnections()
|
||||
session.receiveDone.Wait()
|
||||
if session.ipsecHandle != nil {
|
||||
if err := session.ipsecHandle.Close(ctx); err != nil {
|
||||
// XFRM teardown is local and must still run when SIP deregistration has
|
||||
// consumed the caller's deadline. Use a fresh bounded context so a
|
||||
// service restart or Profile switch cannot strand the previous SIM's
|
||||
// transport-mode policies in the kernel.
|
||||
cleanupContext, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
err := session.ipsecHandle.Close(cleanupContext)
|
||||
cleanupCancel()
|
||||
if err != nil {
|
||||
cleanupErrors = append(cleanupErrors, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +379,69 @@ func serveRegistration(listener *net.UDPConn, nonce string, confirmSMS bool) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestO2GermanyInitialRegisterMatchesSupportedIMSProfile(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
identity := vowifi.SIMIdentity{
|
||||
IMSI: "262030123456789",
|
||||
HomeMCC: "262",
|
||||
HomeMNC: "03",
|
||||
}
|
||||
identities, err := deriveIdentities(identity, Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("deriveIdentities() error = %v", err)
|
||||
}
|
||||
session := &Session{
|
||||
provider: &Provider{config: Config{
|
||||
SecurityMode: SecurityRequired,
|
||||
UserAgent: "vocat-test",
|
||||
}},
|
||||
request: vowifi.IMSRequest{Identity: identity},
|
||||
identity: identities,
|
||||
endpoint: pcscfEndpoint{host: "pcscf.example", port: 5060},
|
||||
transport: "tcp",
|
||||
conn: client,
|
||||
callID: "o2-test",
|
||||
fromTag: "tag",
|
||||
instanceID: "urn:uuid:test",
|
||||
securityProposal: securityProposal{
|
||||
spiClient: 101,
|
||||
spiServer: 102,
|
||||
portClient: 5062,
|
||||
portServer: 5063,
|
||||
encryption: "null",
|
||||
},
|
||||
}
|
||||
|
||||
packet, err := session.buildRegister(1, 3600, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildRegister() error = %v", err)
|
||||
}
|
||||
_, headers, err := parseTestRequest(packet)
|
||||
if err != nil {
|
||||
t.Fatalf("parseTestRequest() error = %v", err)
|
||||
}
|
||||
if got, want := headers["security-client"], "ipsec-3gpp;q=1.000;alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=null;spi-c=0000000101;spi-s=0000000102;port-c=5062;port-s=5063"; got != want {
|
||||
t.Fatalf("Security-Client = %q, want %q", got, want)
|
||||
}
|
||||
if headers["proxy-require"] != "sec-agree" || !strings.Contains(headers["authorization"], "integrity-protected=no") {
|
||||
t.Fatalf("initial O2 headers omitted standardized sec-agree/IMS-AKA fields: %#v", headers)
|
||||
}
|
||||
if got, want := headers["p-preferred-identity"], "<"+identities.public+">"; got != want {
|
||||
t.Fatalf("P-Preferred-Identity = %q, want %q", got, want)
|
||||
}
|
||||
for name, token := range map[string]string{
|
||||
"supported": "sec-agree",
|
||||
"allow": "MESSAGE",
|
||||
} {
|
||||
if !strings.Contains(headers[name], token) {
|
||||
t.Fatalf("%s = %q, want token %q", name, headers[name], token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveRefreshFailure(listener *net.UDPConn, nonce string) error {
|
||||
var callID string
|
||||
for step := 0; step < 3; step++ {
|
||||
|
||||
+197
-47
@@ -39,6 +39,12 @@ var (
|
||||
type IPSecSAConfig struct {
|
||||
LocalIP net.IP
|
||||
RemoteIP net.IP
|
||||
// IntegrityAlgorithm is the negotiated 3GPP Security-Server alg value.
|
||||
// Supported values are hmac-md5-96 and hmac-sha-1-96.
|
||||
IntegrityAlgorithm string
|
||||
// EncryptionAlgorithm is the negotiated 3GPP Security-Server ealg value.
|
||||
// Supported values are null, des-ede3-cbc, and aes-cbc.
|
||||
EncryptionAlgorithm string
|
||||
|
||||
UEClientSPI uint32
|
||||
UEServerSPI uint32
|
||||
@@ -63,10 +69,14 @@ type IPSecSAInstaller interface {
|
||||
}
|
||||
|
||||
type securityProposal struct {
|
||||
spiClient uint32
|
||||
spiServer uint32
|
||||
portClient int
|
||||
portServer int
|
||||
spiClient uint32
|
||||
spiServer uint32
|
||||
portClient int
|
||||
portServer int
|
||||
encryption string
|
||||
fallbackEncryption string
|
||||
integrityAlgorithms []string
|
||||
encryptionAlgorithmsList []string
|
||||
}
|
||||
|
||||
func newSecurityProposal(localIP net.IP, configuredClientPort int, configuredServerPort int) (securityProposal, error) {
|
||||
@@ -96,21 +106,85 @@ func newSecurityProposal(localIP net.IP, configuredClientPort int, configuredSer
|
||||
return securityProposal{}, errors.New("ims: protected UE ports must be distinct non-standard SIP ports")
|
||||
}
|
||||
return securityProposal{
|
||||
spiClient: spiClient,
|
||||
spiServer: spiServer,
|
||||
portClient: portClient,
|
||||
portServer: portServer,
|
||||
spiClient: spiClient,
|
||||
spiServer: spiServer,
|
||||
portClient: portClient,
|
||||
portServer: portServer,
|
||||
integrityAlgorithms: []string{"hmac-md5-96", "hmac-sha-1-96"},
|
||||
encryptionAlgorithmsList: []string{"null", "des-ede3-cbc", "aes-cbc"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (proposal securityProposal) headerValue() string {
|
||||
return fmt.Sprintf(
|
||||
"ipsec-3gpp;q=1.000;alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=aes-cbc;spi-c=%010d;spi-s=%010d;port-c=%d;port-s=%d",
|
||||
proposal.spiClient,
|
||||
proposal.spiServer,
|
||||
proposal.portClient,
|
||||
proposal.portServer,
|
||||
)
|
||||
// TS 33.203 defines spi-c and spi-s as exactly 10 decimal digits. Keep the
|
||||
// complete mechanism explicit even where ESP/null defaults would permit a
|
||||
// shorter form; this is the interoperable handset/IMS profile.
|
||||
values := make([]string, 0, len(proposal.integrities())*len(proposal.encryptionAlgorithms()))
|
||||
index := 0
|
||||
for _, integrity := range proposal.integrities() {
|
||||
for _, encryption := range proposal.encryptionAlgorithms() {
|
||||
preference := fmt.Sprintf("0.%03d", 999-index)
|
||||
if index == 0 {
|
||||
preference = "1.000"
|
||||
}
|
||||
values = append(values, fmt.Sprintf(
|
||||
"ipsec-3gpp;q=%s;alg=%s;prot=esp;mod=trans;ealg=%s;spi-c=%010d;spi-s=%010d;port-c=%d;port-s=%d",
|
||||
preference,
|
||||
integrity,
|
||||
encryption,
|
||||
proposal.spiClient,
|
||||
proposal.spiServer,
|
||||
proposal.portClient,
|
||||
proposal.portServer,
|
||||
))
|
||||
index++
|
||||
}
|
||||
}
|
||||
return strings.Join(values, ", ")
|
||||
}
|
||||
|
||||
func (proposal securityProposal) encryptionAlgorithm() string {
|
||||
if strings.EqualFold(strings.TrimSpace(proposal.encryption), "null") {
|
||||
return "null"
|
||||
}
|
||||
return "aes-cbc"
|
||||
}
|
||||
|
||||
func (proposal securityProposal) encryptionAlgorithms() []string {
|
||||
if len(proposal.encryptionAlgorithmsList) > 0 {
|
||||
return append([]string(nil), proposal.encryptionAlgorithmsList...)
|
||||
}
|
||||
algorithms := []string{proposal.encryptionAlgorithm()}
|
||||
fallback := strings.ToLower(strings.TrimSpace(proposal.fallbackEncryption))
|
||||
if (fallback == "aes-cbc" || fallback == "null") && fallback != algorithms[0] {
|
||||
algorithms = append(algorithms, fallback)
|
||||
}
|
||||
return algorithms
|
||||
}
|
||||
|
||||
func (proposal securityProposal) integrities() []string {
|
||||
if len(proposal.integrityAlgorithms) > 0 {
|
||||
return append([]string(nil), proposal.integrityAlgorithms...)
|
||||
}
|
||||
return []string{"hmac-sha-1-96"}
|
||||
}
|
||||
|
||||
func (proposal securityProposal) supportsIntegrity(integrity string) bool {
|
||||
for _, offered := range proposal.integrities() {
|
||||
if strings.EqualFold(integrity, offered) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (proposal securityProposal) supportsEncryption(encryption string) bool {
|
||||
for _, offered := range proposal.encryptionAlgorithms() {
|
||||
if strings.EqualFold(encryption, offered) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func randomSPI(exclude uint32) (uint32, error) {
|
||||
@@ -198,10 +272,10 @@ func parseSecurityAgreement(values []string, proposal securityProposal) (securit
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(mechanism.name, "ipsec-3gpp") ||
|
||||
!strings.EqualFold(mechanism.algorithm, "hmac-sha-1-96") ||
|
||||
!proposal.supportsIntegrity(mechanism.algorithm) ||
|
||||
!strings.EqualFold(mechanism.protocol, "esp") ||
|
||||
!strings.EqualFold(mechanism.mode, "trans") ||
|
||||
!strings.EqualFold(mechanism.encryption, "aes-cbc") {
|
||||
!proposal.supportsEncryption(mechanism.encryption) {
|
||||
continue
|
||||
}
|
||||
if mechanism.spiClient == 0 || mechanism.spiServer == 0 ||
|
||||
@@ -349,16 +423,48 @@ func preferenceValue(value string) (int, error) {
|
||||
return numeric, nil
|
||||
}
|
||||
|
||||
func expandIPSecKeys(ck []byte, ik []byte) (encryption []byte, integrity []byte, err error) {
|
||||
func expandIPSecKeys(ck []byte, ik []byte, encryptionAlgorithm, integrityAlgorithm string) (encryption []byte, integrity []byte, err error) {
|
||||
if len(ck) != 16 || len(ik) != 16 {
|
||||
return nil, nil, errors.New("ims: AKA did not return 16-byte CK and IK")
|
||||
}
|
||||
encryption = append([]byte(nil), ck...)
|
||||
integrity = make([]byte, 20)
|
||||
copy(integrity, ik)
|
||||
switch strings.ToLower(strings.TrimSpace(encryptionAlgorithm)) {
|
||||
case "null":
|
||||
encryption = nil
|
||||
case "aes-cbc", "":
|
||||
encryption = append([]byte(nil), ck...)
|
||||
case "des-ede3-cbc":
|
||||
encryption = append(encryption, ck...)
|
||||
encryption = append(encryption, ck[:8]...)
|
||||
for index, value := range encryption {
|
||||
encryption[index] = withOddDESParity(value)
|
||||
}
|
||||
default:
|
||||
return nil, nil, errors.New("ims: unsupported ipsec-3gpp encryption algorithm")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(integrityAlgorithm)) {
|
||||
case "hmac-md5-96":
|
||||
integrity = append([]byte(nil), ik...)
|
||||
case "hmac-sha-1-96", "":
|
||||
integrity = make([]byte, 20)
|
||||
copy(integrity, ik)
|
||||
default:
|
||||
return nil, nil, errors.New("ims: unsupported ipsec-3gpp integrity algorithm")
|
||||
}
|
||||
return encryption, integrity, nil
|
||||
}
|
||||
|
||||
func withOddDESParity(value byte) byte {
|
||||
value &^= 1
|
||||
ones := 0
|
||||
for bits := value; bits != 0; bits >>= 1 {
|
||||
ones += int(bits & 1)
|
||||
}
|
||||
if ones%2 == 0 {
|
||||
value |= 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type xfrmOperation struct {
|
||||
description string
|
||||
arguments []string
|
||||
@@ -382,20 +488,31 @@ func buildXFRMInstallPlan(config IPSecSAConfig) ([]xfrmOperation, error) {
|
||||
{"outbound UE-server to P-CSCF-client state", config.LocalIP, config.RemoteIP, config.PCSCFClientSPI, serverPairReqID(config)},
|
||||
}
|
||||
for _, state := range states {
|
||||
arguments := []string{
|
||||
"xfrm", "state", "add",
|
||||
"src", state.source.String(),
|
||||
"dst", state.destination.String(),
|
||||
"proto", "esp",
|
||||
"spi", fmt.Sprintf("0x%08x", state.spi),
|
||||
"reqid", strconv.FormatUint(uint64(state.reqid), 10),
|
||||
"mode", "transport",
|
||||
"replay-window", "32",
|
||||
"auth-trunc", xfrmIntegrityAlgorithm(config), "0x" + hex.EncodeToString(config.IntegrityKey), "96",
|
||||
}
|
||||
switch ipsecEncryptionAlgorithm(config) {
|
||||
case "aes-cbc":
|
||||
arguments = append(arguments, "enc", "cbc(aes)", "0x"+hex.EncodeToString(config.EncryptionKey))
|
||||
case "des-ede3-cbc":
|
||||
arguments = append(arguments, "enc", "cbc(des3_ede)", "0x"+hex.EncodeToString(config.EncryptionKey))
|
||||
default:
|
||||
// Linux requires an explicit encryption transform for ESP even when
|
||||
// 3GPP negotiates ealg=null. iproute2 must receive a genuinely empty
|
||||
// key argument; the textual value "0x" is rejected by XFRM as EINVAL.
|
||||
arguments = append(arguments, "enc", "cipher_null", "")
|
||||
}
|
||||
operations = append(operations, xfrmOperation{
|
||||
description: state.description,
|
||||
arguments: []string{
|
||||
"xfrm", "state", "add",
|
||||
"src", state.source.String(),
|
||||
"dst", state.destination.String(),
|
||||
"proto", "esp",
|
||||
"spi", fmt.Sprintf("0x%08x", state.spi),
|
||||
"reqid", strconv.FormatUint(uint64(state.reqid), 10),
|
||||
"mode", "transport",
|
||||
"replay-window", "32",
|
||||
"auth-trunc", "hmac(sha1)", "0x" + hex.EncodeToString(config.IntegrityKey), "96",
|
||||
"enc", "cbc(aes)", "0x" + hex.EncodeToString(config.EncryptionKey),
|
||||
},
|
||||
arguments: arguments,
|
||||
})
|
||||
}
|
||||
for _, flow := range xfrmFlows(config) {
|
||||
@@ -588,12 +705,43 @@ func validateIPSecSAConfig(config IPSecSAConfig) error {
|
||||
config.PCSCFClientPort == config.PCSCFServerPort {
|
||||
return errors.New("ims: client and server protected ports must differ")
|
||||
}
|
||||
if len(config.EncryptionKey) != 16 || len(config.IntegrityKey) != 20 {
|
||||
if ipsecEncryptionAlgorithm(config) != "null" && ipsecEncryptionAlgorithm(config) != "aes-cbc" && ipsecEncryptionAlgorithm(config) != "des-ede3-cbc" {
|
||||
return errors.New("ims: unsupported ipsec-3gpp encryption algorithm")
|
||||
}
|
||||
if ipsecIntegrityAlgorithm(config) != "hmac-md5-96" && ipsecIntegrityAlgorithm(config) != "hmac-sha-1-96" {
|
||||
return errors.New("ims: unsupported ipsec-3gpp integrity algorithm")
|
||||
}
|
||||
wantEncryptionKey := map[string]int{"null": 0, "aes-cbc": 16, "des-ede3-cbc": 24}[ipsecEncryptionAlgorithm(config)]
|
||||
wantIntegrityKey := map[string]int{"hmac-md5-96": 16, "hmac-sha-1-96": 20}[ipsecIntegrityAlgorithm(config)]
|
||||
if len(config.EncryptionKey) != wantEncryptionKey || len(config.IntegrityKey) != wantIntegrityKey {
|
||||
return errors.New("ims: ipsec-3gpp key length is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipsecIntegrityAlgorithm(config IPSecSAConfig) string {
|
||||
algorithm := strings.ToLower(strings.TrimSpace(config.IntegrityAlgorithm))
|
||||
if algorithm == "" {
|
||||
return "hmac-sha-1-96"
|
||||
}
|
||||
return algorithm
|
||||
}
|
||||
|
||||
func xfrmIntegrityAlgorithm(config IPSecSAConfig) string {
|
||||
if ipsecIntegrityAlgorithm(config) == "hmac-md5-96" {
|
||||
return "hmac(md5)"
|
||||
}
|
||||
return "hmac(sha1)"
|
||||
}
|
||||
|
||||
func ipsecEncryptionAlgorithm(config IPSecSAConfig) string {
|
||||
algorithm := strings.ToLower(strings.TrimSpace(config.EncryptionAlgorithm))
|
||||
if algorithm == "" {
|
||||
return "aes-cbc"
|
||||
}
|
||||
return algorithm
|
||||
}
|
||||
|
||||
func cloneIPSecSAConfig(config IPSecSAConfig) IPSecSAConfig {
|
||||
config.LocalIP = append(net.IP(nil), config.LocalIP...)
|
||||
config.RemoteIP = append(net.IP(nil), config.RemoteIP...)
|
||||
@@ -657,7 +805,7 @@ func (session *Session) activateIPSec(
|
||||
if !session.securityOffered() {
|
||||
return ErrIPSecAgreementRequired
|
||||
}
|
||||
encryptionKey, integrityKey, err := expandIPSecKeys(ck, ik)
|
||||
encryptionKey, integrityKey, err := expandIPSecKeys(ck, ik, agreement.selected.encryption, agreement.selected.algorithm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -671,18 +819,20 @@ func (session *Session) activateIPSec(
|
||||
}
|
||||
selected := agreement.selected
|
||||
config := IPSecSAConfig{
|
||||
LocalIP: localIP,
|
||||
RemoteIP: remoteIP,
|
||||
UEClientSPI: session.securityProposal.spiClient,
|
||||
UEServerSPI: session.securityProposal.spiServer,
|
||||
PCSCFClientSPI: selected.spiClient,
|
||||
PCSCFServerSPI: selected.spiServer,
|
||||
UEClientPort: session.securityProposal.portClient,
|
||||
UEServerPort: session.securityProposal.portServer,
|
||||
PCSCFClientPort: selected.portClient,
|
||||
PCSCFServerPort: selected.portServer,
|
||||
EncryptionKey: encryptionKey,
|
||||
IntegrityKey: integrityKey,
|
||||
LocalIP: localIP,
|
||||
RemoteIP: remoteIP,
|
||||
IntegrityAlgorithm: selected.algorithm,
|
||||
EncryptionAlgorithm: selected.encryption,
|
||||
UEClientSPI: session.securityProposal.spiClient,
|
||||
UEServerSPI: session.securityProposal.spiServer,
|
||||
PCSCFClientSPI: selected.spiClient,
|
||||
PCSCFServerSPI: selected.spiServer,
|
||||
UEClientPort: session.securityProposal.portClient,
|
||||
UEServerPort: session.securityProposal.portServer,
|
||||
PCSCFClientPort: selected.portClient,
|
||||
PCSCFServerPort: selected.portServer,
|
||||
EncryptionKey: encryptionKey,
|
||||
IntegrityKey: integrityKey,
|
||||
}
|
||||
handle, err := session.provider.installer.Install(ctx, config)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,8 +23,9 @@ type fakeIPSecInstaller struct {
|
||||
}
|
||||
|
||||
type fakeIPSecHandle struct {
|
||||
mu sync.Mutex
|
||||
closeCount int
|
||||
mu sync.Mutex
|
||||
closeCount int
|
||||
closeContextErr error
|
||||
}
|
||||
|
||||
func (installer *fakeIPSecInstaller) Install(
|
||||
@@ -53,10 +54,11 @@ func (installer *fakeIPSecInstaller) installed() []IPSecSAConfig {
|
||||
return result
|
||||
}
|
||||
|
||||
func (handle *fakeIPSecHandle) Close(context.Context) error {
|
||||
func (handle *fakeIPSecHandle) Close(ctx context.Context) error {
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
handle.closeCount++
|
||||
handle.closeContextErr = ctx.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -66,6 +68,38 @@ func (handle *fakeIPSecHandle) closes() int {
|
||||
return handle.closeCount
|
||||
}
|
||||
|
||||
func (handle *fakeIPSecHandle) contextError() error {
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
return handle.closeContextErr
|
||||
}
|
||||
|
||||
func TestSessionCloseCleansIPSecAfterCallerDeadline(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer server.Close()
|
||||
refreshDone := make(chan struct{})
|
||||
close(refreshDone)
|
||||
handle := &fakeIPSecHandle{}
|
||||
session := &Session{
|
||||
conn: client,
|
||||
refreshCancel: func() {},
|
||||
refreshDone: refreshDone,
|
||||
ipsecHandle: handle,
|
||||
calls: make(map[string]*imsCall),
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := session.Close(ctx); err != nil {
|
||||
t.Fatalf("Close() error = %v", err)
|
||||
}
|
||||
if handle.closes() != 1 {
|
||||
t.Fatalf("IPsec close count = %d, want 1", handle.closes())
|
||||
}
|
||||
if err := handle.contextError(); err != nil {
|
||||
t.Fatalf("IPsec cleanup inherited expired caller context: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderNegotiatesIPSecAndRegistersOverProtectedTCP(t *testing.T) {
|
||||
localIP := net.ParseIP("127.0.0.1")
|
||||
remoteIP := net.ParseIP("127.0.0.2")
|
||||
@@ -386,7 +420,12 @@ func serveProtectedRegistrar(
|
||||
return result, fmt.Errorf("initial sec-agree headers = %#v", headers)
|
||||
}
|
||||
result.securityClient = headers["security-client"]
|
||||
proposal, err := parseSecurityMechanism(result.securityClient)
|
||||
offers := splitHeaderValues([]string{result.securityClient})
|
||||
if len(offers) != 6 {
|
||||
_ = initialConnection.Close()
|
||||
return result, fmt.Errorf("initial Security-Client offers = %d, want 6", len(offers))
|
||||
}
|
||||
proposal, err := parseSecurityMechanism(offers[0])
|
||||
if err != nil {
|
||||
_ = initialConnection.Close()
|
||||
return result, fmt.Errorf("parse initial Security-Client: %w", err)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"vocat/internal/vowifi"
|
||||
)
|
||||
|
||||
func TestParseSecurityAgreementSelectsSupportedIPSec(t *testing.T) {
|
||||
@@ -36,6 +38,51 @@ func TestParseSecurityAgreementSelectsSupportedIPSec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestO2GermanySecurityProposalUsesIntegrityOnlyESP(t *testing.T) {
|
||||
identity := vowifi.SIMIdentity{HomeMCC: "262", HomeMNC: "03"}
|
||||
if got := securityEncryptionForIdentity(identity); got != "null" {
|
||||
t.Fatalf("O2 security encryption = %q, want null", got)
|
||||
}
|
||||
proposal := securityProposal{
|
||||
spiClient: 1001, spiServer: 1002,
|
||||
portClient: 40666, portServer: 55610,
|
||||
encryption: securityEncryptionForIdentity(identity),
|
||||
}
|
||||
if got, want := proposal.headerValue(), "ipsec-3gpp;q=1.000;alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=null;spi-c=0000001001;spi-s=0000001002;port-c=40666;port-s=55610"; got != want {
|
||||
t.Fatalf("O2 Security-Client = %q, want %q", got, want)
|
||||
}
|
||||
selected := "ipsec-3gpp;q=1.000;alg=hmac-sha-1-96;prot=esp;mod=trans;" +
|
||||
"ealg=null;spi-c=2001;spi-s=2002;port-c=50601;port-s=50600"
|
||||
if _, err := parseSecurityAgreement([]string{selected}, proposal); err != nil {
|
||||
t.Fatalf("O2 null Security-Server rejected: %v", err)
|
||||
}
|
||||
|
||||
identity.HomeMNC = "02"
|
||||
if got := securityEncryptionForIdentity(identity); got != "aes-cbc" {
|
||||
t.Fatalf("non-O2 security encryption = %q, want aes-cbc", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityAgreementAcceptsO2FallbackEncryption(t *testing.T) {
|
||||
proposal := securityProposal{
|
||||
spiClient: 1001,
|
||||
spiServer: 1002,
|
||||
portClient: 5062,
|
||||
portServer: 5063,
|
||||
encryption: "null",
|
||||
fallbackEncryption: "aes-cbc",
|
||||
}
|
||||
value := "ipsec-3gpp;q=0.5;alg=hmac-sha-1-96;prot=esp;mod=trans;" +
|
||||
"ealg=aes-cbc;spi-c=2001;spi-s=2002;port-c=50601;port-s=50600"
|
||||
agreement, err := parseSecurityAgreement([]string{value}, proposal)
|
||||
if err != nil {
|
||||
t.Fatalf("parseSecurityAgreement(aes-cbc fallback) error = %v", err)
|
||||
}
|
||||
if got := agreement.selected.encryption; got != "aes-cbc" {
|
||||
t.Fatalf("selected encryption = %q, want aes-cbc", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSecurityAgreementSkipsIncompleteCarrierAlternatives(t *testing.T) {
|
||||
proposal := securityProposal{
|
||||
spiClient: 1001,
|
||||
@@ -121,7 +168,7 @@ func TestParseSecurityAgreementFailsClosed(t *testing.T) {
|
||||
func TestExpandIPSecKeys(t *testing.T) {
|
||||
ck := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||
ik := []byte{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
|
||||
encryption, integrity, err := expandIPSecKeys(ck, ik)
|
||||
encryption, integrity, err := expandIPSecKeys(ck, ik, "aes-cbc", "hmac-sha-1-96")
|
||||
if err != nil {
|
||||
t.Fatalf("expandIPSecKeys() error = %v", err)
|
||||
}
|
||||
@@ -139,6 +186,71 @@ func TestExpandIPSecKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandIPSecKeysForAndroidAlgorithmSet(t *testing.T) {
|
||||
ck := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}
|
||||
ik := []byte{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38}
|
||||
tripleDES, md5Key, err := expandIPSecKeys(ck, ik, "des-ede3-cbc", "hmac-md5-96")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tripleDES) != 24 || len(md5Key) != 16 {
|
||||
t.Fatalf("3DES/MD5 key lengths = %d/%d", len(tripleDES), len(md5Key))
|
||||
}
|
||||
for _, value := range tripleDES {
|
||||
ones := 0
|
||||
for bits := value; bits != 0; bits >>= 1 {
|
||||
ones += int(bits & 1)
|
||||
}
|
||||
if ones%2 != 1 {
|
||||
t.Fatalf("3DES byte %02x does not have odd parity", value)
|
||||
}
|
||||
}
|
||||
nullKey, sha1Key, err := expandIPSecKeys(ck, ik, "null", "hmac-sha-1-96")
|
||||
if err != nil || len(nullKey) != 0 || len(sha1Key) != 20 {
|
||||
t.Fatalf("null/SHA1 keys = %d/%d, %v", len(nullKey), len(sha1Key), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndroidIMSProposalOffersAllDefaultAlgorithms(t *testing.T) {
|
||||
proposal := securityProposal{
|
||||
spiClient: 1001, spiServer: 1002, portClient: 5062, portServer: 5063,
|
||||
integrityAlgorithms: []string{"hmac-md5-96", "hmac-sha-1-96"},
|
||||
encryptionAlgorithmsList: []string{"null", "des-ede3-cbc", "aes-cbc"},
|
||||
}
|
||||
header := proposal.headerValue()
|
||||
for _, combination := range []string{
|
||||
"alg=hmac-md5-96;prot=esp;mod=trans;ealg=null",
|
||||
"alg=hmac-md5-96;prot=esp;mod=trans;ealg=des-ede3-cbc",
|
||||
"alg=hmac-md5-96;prot=esp;mod=trans;ealg=aes-cbc",
|
||||
"alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=null",
|
||||
"alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=des-ede3-cbc",
|
||||
"alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=aes-cbc",
|
||||
} {
|
||||
if !strings.Contains(header, combination) {
|
||||
t.Fatalf("Android IMS Security-Client omitted %q: %s", combination, header)
|
||||
}
|
||||
}
|
||||
if got := len(splitHeaderValues([]string{header})); got != 6 {
|
||||
t.Fatalf("Security-Client mechanism count = %d, want 6", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSecurityProposalUsesAndroidDefaultsForEveryCarrier(t *testing.T) {
|
||||
proposal, err := newSecurityProposal(net.ParseIP("127.0.0.1"), 45062, 45063)
|
||||
if err != nil {
|
||||
t.Fatalf("newSecurityProposal() error = %v", err)
|
||||
}
|
||||
if got, want := proposal.integrities(), []string{"hmac-md5-96", "hmac-sha-1-96"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("integrity algorithms = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := proposal.encryptionAlgorithms(), []string{"null", "des-ede3-cbc", "aes-cbc"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("encryption algorithms = %v, want %v", got, want)
|
||||
}
|
||||
if got := len(splitHeaderValues([]string{proposal.headerValue()})); got != 6 {
|
||||
t.Fatalf("Security-Client mechanism count = %d, want 6", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXFRMPlanContainsFourStatesAndProtocolSpecificPolicies(t *testing.T) {
|
||||
config := testIPSecSAConfig()
|
||||
install, err := buildXFRMInstallPlan(config)
|
||||
@@ -209,6 +321,46 @@ func TestXFRMPlanContainsFourStatesAndProtocolSpecificPolicies(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXFRMPlanSupportsIntegrityOnlyESP(t *testing.T) {
|
||||
config := testIPSecSAConfig()
|
||||
config.EncryptionAlgorithm = "null"
|
||||
config.EncryptionKey = nil
|
||||
install, err := buildXFRMInstallPlan(config)
|
||||
if err != nil {
|
||||
t.Fatalf("buildXFRMInstallPlan(null) error = %v", err)
|
||||
}
|
||||
for index, operation := range install[:4] {
|
||||
joined := strings.Join(operation.arguments, " ")
|
||||
if !strings.Contains(joined, "auth-trunc hmac(sha1)") {
|
||||
t.Fatalf("null state %d omitted integrity: %v", index, operation.arguments)
|
||||
}
|
||||
if !containsArguments(operation.arguments, "enc", "cipher_null", "") {
|
||||
t.Fatalf("null state %d omitted Linux NULL cipher: %v", index, operation.arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXFRMPlanSupportsAndroidMD5AndTripleDES(t *testing.T) {
|
||||
config := testIPSecSAConfig()
|
||||
config.IntegrityAlgorithm = "hmac-md5-96"
|
||||
config.EncryptionAlgorithm = "des-ede3-cbc"
|
||||
config.IntegrityKey = []byte(strings.Repeat("\x22", 16))
|
||||
config.EncryptionKey = []byte(strings.Repeat("\x11", 24))
|
||||
|
||||
install, err := buildXFRMInstallPlan(config)
|
||||
if err != nil {
|
||||
t.Fatalf("buildXFRMInstallPlan() error = %v", err)
|
||||
}
|
||||
for index, operation := range install[:4] {
|
||||
if !containsArguments(operation.arguments, "auth-trunc", "hmac(md5)", "0x"+strings.Repeat("22", 16), "96") {
|
||||
t.Fatalf("state %d omitted HMAC-MD5-96 transform: %v", index, operation.arguments)
|
||||
}
|
||||
if !containsArguments(operation.arguments, "enc", "cbc(des3_ede)", "0x"+strings.Repeat("11", 24)) {
|
||||
t.Fatalf("state %d omitted 3DES-CBC transform: %v", index, operation.arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIPSecSAConfigRejectsDuplicateSPI(t *testing.T) {
|
||||
config := testIPSecSAConfig()
|
||||
config.PCSCFServerSPI = config.UEClientSPI
|
||||
|
||||
Reference in New Issue
Block a user