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:
MengMengCode
2026-08-13 23:30:00 +08:00
parent 64cd5714ef
commit 30c0d2d9d5
13 changed files with 844 additions and 84 deletions
+4
View File
@@ -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 {
+57
View File
@@ -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 != ""
}
+39
View File
@@ -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)
}
}
+62 -14
View File
@@ -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))
}
}
+50
View File
@@ -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()
+45 -1
View File
@@ -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,
)
+9 -5
View File
@@ -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 (
+61 -5
View File
@@ -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")
}
}
+61 -7
View File
@@ -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)
}
}
+63
View File
@@ -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
View File
@@ -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 {
+43 -4
View File
@@ -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)
+153 -1
View File
@@ -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