package ike import ( "bytes" "errors" "testing" "vocat/internal/vowifi" ) func TestPayloadAndProposalWireRoundTrip(t *testing.T) { offered := proposal{ Number: 1, Protocol: protocolIKE, Transforms: []transform{ {Type: transformEncryption, ID: encryptionAESCBC, KeyLength: 128}, {Type: transformPRF, ID: prfHMACSHA1}, {Type: transformIntegrity, ID: integrityHMACSHA1_96}, {Type: transformDH, ID: dhMODP1024}, }, } sa, err := marshalProposals([]proposal{offered}) if err != nil { t.Fatalf("marshalProposals() error = %v", err) } first, wire, err := marshalPayloadChain([]payload{ {Type: payloadSA, Body: sa}, {Type: payloadNonce, Body: bytes.Repeat([]byte{0xaa}, 32)}, }) if err != nil { t.Fatalf("marshalPayloadChain() error = %v", err) } if first != payloadSA || len(wire) < 8 || wire[0] != payloadNonce { t.Fatalf("unexpected payload chain header: first=%d wire=%x", first, wire[:8]) } decoded, err := parsePayloadChain(first, wire) if err != nil { t.Fatalf("parsePayloadChain() error = %v", err) } if len(decoded) != 2 || decoded[0].Type != payloadSA || decoded[1].Type != payloadNonce { t.Fatalf("decoded payload chain = %#v", decoded) } proposals, err := parseProposals(decoded[0].Body) if err != nil { t.Fatalf("parseProposals() error = %v", err) } if len(proposals) != 1 || len(proposals[0].Transforms) != 4 || proposals[0].Transforms[0].KeyLength != 128 { t.Fatalf("decoded proposal = %#v", proposals) } } func TestInitialEAPOnlyAuthCarriesAPNIDrAndNotify(t *testing.T) { idi := payload{Type: payloadIDi, Body: []byte{3, 0, 0, 0, 'u'}} idr := payload{Type: payloadIDr, Body: []byte{2, 0, 0, 0, 'i', 'm', 's'}} payloads := buildInitialEAPOnlyAuth( idi, idr, []byte{1, 2, 3}, dualStackTrafficSelectors(payloadTSi), dualStackTrafficSelectors(payloadTSr), ) if len(payloads) != 7 || 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 { t.Fatalf("IDr = type %d value %q, want ID_FQDN ims", payloads[1].Body[0], got) } kind, data, err := parseNotify(payloads[2]) if err != nil { t.Fatalf("parseNotify() error = %v", err) } if kind != notifyEAPOnlyAuth || len(data) != 0 { t.Fatalf("notify = %d/%x, want EAP_ONLY_AUTHENTICATION", kind, data) } for _, kind := range []uint8{payloadTSi, payloadTSr} { item, err := onePayload(payloads, kind) if err != nil { t.Fatalf("onePayload(%d) error = %v", kind, err) } selectors, err := parseTrafficSelectors(item) if err != nil { t.Fatalf("parseTrafficSelectors(%d) error = %v", kind, err) } if len(selectors) != 2 || selectors[0].StartIP.To4() == nil || selectors[1].StartIP.To4() != nil { t.Fatalf("dual-stack selectors = %#v", selectors) } } } func TestResponderIDrValidatorsSeparateEPDGAndAPN(t *testing.T) { epdg := payload{ Type: payloadIDr, Body: append([]byte{2, 0, 0, 0}, []byte("epdg.epc.mnc015.mcc234.pub.3gppnetwork.org")...), } if err := validateFQDNIDr(epdg, "epdg.epc.mnc015.mcc234.pub.3gppnetwork.org", "initial ePDG"); err != nil { t.Fatalf("valid initial IDr rejected: %v", err) } apn := payload{Type: payloadIDr, Body: []byte{2, 0, 0, 0, 'i', 'm', 's'}} if err := validateFQDNIDr(apn, "ims", "final APN"); err != nil { t.Fatalf("valid final IDr rejected: %v", err) } wrongType := apn wrongType.Body = append([]byte(nil), apn.Body...) wrongType.Body[0] = 11 if err := validateFQDNIDr(wrongType, "ims", "final APN"); err == nil { t.Fatal("non-ID_FQDN final IDr was accepted") } if err := validateFQDNIDr(epdg, "ims", "final APN"); err == nil { t.Fatal("ePDG identity was accepted as the APN identity") } } func TestFinalEAPResponderAUTHValidAndInvalidNeverAllowed(t *testing.T) { suite := legacyTestSuite() msk := bytes.Repeat([]byte{0x10}, 64) initialResponse := bytes.Repeat([]byte{0x20}, 96) initiatorNonce := bytes.Repeat([]byte{0x30}, 32) skpr := bytes.Repeat([]byte{0x40}, 20) idr := payload{Type: payloadIDr, Body: append([]byte{2, 0, 0, 0}, []byte("epdg.example")...)} signed, err := responderSignedOctets(initialResponse, initiatorNonce, suite, skpr, idr) if err != nil { t.Fatalf("responderSignedOctets() error = %v", err) } padded, _ := prf(suite, msk, []byte("Key Pad for IKEv2")) value, _ := prf(suite, padded, signed) auth := payload{Type: payloadAuth, Body: append([]byte{authMethodSharedKeyMIC, 0, 0, 0}, value...)} if err := verifyEAPResponderAUTH(auth, msk, initialResponse, initiatorNonce, suite, skpr, idr); err != nil { t.Fatalf("valid final responder AUTH rejected: %v", err) } auth.Body[len(auth.Body)-1] ^= 1 if err := verifyEAPResponderAUTH(auth, msk, initialResponse, initiatorNonce, suite, skpr, idr); err == nil { t.Fatal("invalid final responder AUTH was accepted") } status, _, err := validateInitialResponderAUTH( []payload{idr}, initialResponse, initiatorNonce, suite, skpr, "epdg.example", "epdg.example", nil, nil, false, ) if status != vowifi.ResponderAUTHMissing || !errors.Is(err, vowifi.ErrResponderAUTHRequired) { t.Fatalf("strict missing initial AUTH = status %q err %v", status, err) } status, _, err = validateInitialResponderAUTH( []payload{idr}, initialResponse, initiatorNonce, suite, skpr, "epdg.example", "epdg.example", nil, nil, true, ) if status != vowifi.ResponderAUTHMissing || err != nil { t.Fatalf("EAP-only deferred initial AUTH = status %q err %v", status, err) } } func TestConfigurationIPv6PrefixIsMandatoryAndPreserved(t *testing.T) { ipv6 := bytes.Repeat([]byte{0x20}, 16) validBody := []byte{configReply, 0, 0, 0, 0, configInternalIPv6Address, 0, 17} validBody = append(validBody, ipv6...) validBody = append(validBody, 64) configuration, err := parseConfiguration(payload{Type: payloadCP, Body: validBody}) if err != nil { t.Fatalf("parseConfiguration(valid IPv6) error = %v", err) } if configuration.IPv6Prefix != 64 || !bytes.Equal(configuration.LocalIPv6, ipv6) { t.Fatalf("IPv6 configuration = %#v", configuration) } invalidBody := []byte{configReply, 0, 0, 0, 0, configInternalIPv6Address, 0, 16} invalidBody = append(invalidBody, ipv6...) if _, err := parseConfiguration(payload{Type: payloadCP, Body: invalidBody}); err == nil { t.Fatal("16-byte INTERNAL_IP6_ADDRESS without prefix was accepted") } }