package link import ( "testing" "time" "git.quad4.io/Networks/Reticulum-Go/pkg/common" "git.quad4.io/Networks/Reticulum-Go/pkg/destination" "git.quad4.io/Networks/Reticulum-Go/pkg/identity" "git.quad4.io/Networks/Reticulum-Go/pkg/packet" "git.quad4.io/Networks/Reticulum-Go/pkg/transport" ) func TestEphemeralKeyGeneration(t *testing.T) { link := &Link{} if err := link.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate ephemeral keys: %v", err) } if len(link.prv) != KEYSIZE { t.Errorf("Expected private key length %d, got %d", KEYSIZE, len(link.prv)) } if len(link.pub) != KEYSIZE { t.Errorf("Expected public key length %d, got %d", KEYSIZE, len(link.pub)) } if len(link.sigPriv) != 64 { t.Errorf("Expected signing private key length 64, got %d", len(link.sigPriv)) } if len(link.sigPub) != 32 { t.Errorf("Expected signing public key length 32, got %d", len(link.sigPub)) } } func TestSignallingBytes(t *testing.T) { mtu := 500 mode := byte(MODE_AES256_CBC) bytes := signallingBytes(mtu, mode) if len(bytes) != LINK_MTU_SIZE { t.Errorf("Expected signalling bytes length %d, got %d", LINK_MTU_SIZE, len(bytes)) } extractedMTU := (int(bytes[0]&0x1F) << 16) | (int(bytes[1]) << 8) | int(bytes[2]) if extractedMTU != mtu { t.Errorf("Expected MTU %d, got %d", mtu, extractedMTU) } extractedMode := (bytes[0] & MODE_BYTEMASK) >> 5 if extractedMode != mode { t.Errorf("Expected mode %d, got %d", mode, extractedMode) } } func TestLinkIDGeneration(t *testing.T) { responderIdent, err := identity.NewIdentity() if err != nil { t.Fatalf("Failed to create responder identity: %v", err) } cfg := &common.ReticulumConfig{} transportInstance := transport.NewTransport(cfg) dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link") if err != nil { t.Fatalf("Failed to create destination: %v", err) } link := &Link{ destination: dest, transport: transportInstance, initiator: true, } if err := link.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate keys: %v", err) } link.mode = MODE_DEFAULT link.mtu = 500 signalling := signallingBytes(link.mtu, link.mode) requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE) requestData = append(requestData, link.pub...) requestData = append(requestData, link.sigPub...) requestData = append(requestData, signalling...) pkt := &packet.Packet{ HeaderType: packet.HeaderType1, PacketType: packet.PacketTypeLinkReq, TransportType: 0, Context: packet.ContextNone, ContextFlag: packet.FlagUnset, Hops: 0, DestinationType: dest.GetType(), DestinationHash: dest.GetHash(), Data: requestData, } if err := pkt.Pack(); err != nil { t.Fatalf("Failed to pack packet: %v", err) } linkID := linkIDFromPacket(pkt) if len(linkID) != 16 { t.Errorf("Expected link ID length 16, got %d", len(linkID)) } t.Logf("Generated link ID: %x", linkID) } func TestHandshake(t *testing.T) { link1 := &Link{} link2 := &Link{} if err := link1.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate keys for link1: %v", err) } if err := link2.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate keys for link2: %v", err) } link1.peerPub = link2.pub link2.peerPub = link1.pub link1.linkID = []byte("test-link-id-abc") link2.linkID = []byte("test-link-id-abc") link1.mode = MODE_AES256_CBC link2.mode = MODE_AES256_CBC if err := link1.performHandshake(); err != nil { t.Fatalf("Link1 handshake failed: %v", err) } if err := link2.performHandshake(); err != nil { t.Fatalf("Link2 handshake failed: %v", err) } if string(link1.sharedKey) != string(link2.sharedKey) { t.Error("Shared keys do not match") } if string(link1.derivedKey) != string(link2.derivedKey) { t.Error("Derived keys do not match") } if link1.status != STATUS_HANDSHAKE { t.Errorf("Expected link1 status HANDSHAKE, got %d", link1.status) } if link2.status != STATUS_HANDSHAKE { t.Errorf("Expected link2 status HANDSHAKE, got %d", link2.status) } } func TestLinkEstablishment(t *testing.T) { responderIdent, err := identity.NewIdentity() if err != nil { t.Fatalf("Failed to create responder identity: %v", err) } cfg := &common.ReticulumConfig{} transportInstance := transport.NewTransport(cfg) dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link") if err != nil { t.Fatalf("Failed to create destination: %v", err) } initiatorLink := &Link{ destination: dest, transport: transportInstance, initiator: true, } responderLink := &Link{ transport: transportInstance, initiator: false, } if err := initiatorLink.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate initiator keys: %v", err) } initiatorLink.mode = MODE_DEFAULT initiatorLink.mtu = 500 signalling := signallingBytes(initiatorLink.mtu, initiatorLink.mode) requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE) requestData = append(requestData, initiatorLink.pub...) requestData = append(requestData, initiatorLink.sigPub...) requestData = append(requestData, signalling...) linkRequestPkt := &packet.Packet{ HeaderType: packet.HeaderType1, PacketType: packet.PacketTypeLinkReq, TransportType: 0, Context: packet.ContextNone, ContextFlag: packet.FlagUnset, Hops: 0, DestinationType: dest.GetType(), DestinationHash: dest.GetHash(), Data: requestData, } if err := linkRequestPkt.Pack(); err != nil { t.Fatalf("Failed to pack link request: %v", err) } initiatorLink.linkID = linkIDFromPacket(linkRequestPkt) initiatorLink.requestTime = time.Now() initiatorLink.status = STATUS_PENDING t.Logf("Initiator link request created, link_id=%x", initiatorLink.linkID) responderLink.peerPub = linkRequestPkt.Data[0:KEYSIZE] responderLink.peerSigPub = linkRequestPkt.Data[KEYSIZE:ECPUBSIZE] responderLink.linkID = linkIDFromPacket(linkRequestPkt) responderLink.initiator = false t.Logf("Responder link ID=%x (len=%d)", responderLink.linkID, len(responderLink.linkID)) if len(responderLink.linkID) == 0 { t.Fatal("Responder link ID is empty!") } if len(linkRequestPkt.Data) >= ECPUBSIZE+LINK_MTU_SIZE { mtuBytes := linkRequestPkt.Data[ECPUBSIZE : ECPUBSIZE+LINK_MTU_SIZE] responderLink.mtu = (int(mtuBytes[0]&0x1F) << 16) | (int(mtuBytes[1]) << 8) | int(mtuBytes[2]) responderLink.mode = (mtuBytes[0] & MODE_BYTEMASK) >> 5 } if err := responderLink.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate responder keys: %v", err) } if err := responderLink.performHandshake(); err != nil { t.Fatalf("Responder handshake failed: %v", err) } responderLink.status = STATUS_ACTIVE responderLink.establishedAt = time.Now() if string(responderLink.linkID) != string(initiatorLink.linkID) { t.Error("Link IDs do not match between initiator and responder") } t.Logf("Responder handshake successful, shared_key_len=%d", len(responderLink.sharedKey)) } func TestLinkProofValidation(t *testing.T) { responderIdent, err := identity.NewIdentity() if err != nil { t.Fatalf("Failed to create responder identity: %v", err) } cfg := &common.ReticulumConfig{} transportInstance := transport.NewTransport(cfg) dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link") if err != nil { t.Fatalf("Failed to create destination: %v", err) } initiatorLink := &Link{ destination: dest, transport: transportInstance, initiator: true, } responderLink := &Link{ transport: transportInstance, initiator: false, } if err := initiatorLink.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate initiator keys: %v", err) } initiatorLink.mode = MODE_DEFAULT initiatorLink.mtu = 500 signalling := signallingBytes(initiatorLink.mtu, initiatorLink.mode) requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE) requestData = append(requestData, initiatorLink.pub...) requestData = append(requestData, initiatorLink.sigPub...) requestData = append(requestData, signalling...) linkRequestPkt := &packet.Packet{ HeaderType: packet.HeaderType1, PacketType: packet.PacketTypeLinkReq, TransportType: 0, Context: packet.ContextNone, ContextFlag: packet.FlagUnset, Hops: 0, DestinationType: dest.GetType(), DestinationHash: dest.GetHash(), Data: requestData, } if err := linkRequestPkt.Pack(); err != nil { t.Fatalf("Failed to pack link request: %v", err) } initiatorLink.linkID = linkIDFromPacket(linkRequestPkt) initiatorLink.requestTime = time.Now() initiatorLink.status = STATUS_PENDING responderLink.peerPub = linkRequestPkt.Data[0:KEYSIZE] responderLink.peerSigPub = linkRequestPkt.Data[KEYSIZE:ECPUBSIZE] responderLink.linkID = linkIDFromPacket(linkRequestPkt) responderLink.initiator = false if len(linkRequestPkt.Data) >= ECPUBSIZE+LINK_MTU_SIZE { mtuBytes := linkRequestPkt.Data[ECPUBSIZE : ECPUBSIZE+LINK_MTU_SIZE] responderLink.mtu = (int(mtuBytes[0]&0x1F) << 16) | (int(mtuBytes[1]) << 8) | int(mtuBytes[2]) responderLink.mode = (mtuBytes[0] & MODE_BYTEMASK) >> 5 } else { responderLink.mtu = 500 responderLink.mode = MODE_DEFAULT } if err := responderLink.generateEphemeralKeys(); err != nil { t.Fatalf("Failed to generate responder keys: %v", err) } if err := responderLink.performHandshake(); err != nil { t.Fatalf("Responder handshake failed: %v", err) } proofPkt, err := responderLink.GenerateLinkProof(responderIdent) if err != nil { t.Fatalf("Failed to generate link proof: %v", err) } if err := initiatorLink.ValidateLinkProof(proofPkt, nil); err != nil { t.Fatalf("Initiator failed to validate link proof: %v", err) } if initiatorLink.status != STATUS_ACTIVE { t.Errorf("Expected initiator status ACTIVE, got %d", initiatorLink.status) } if string(initiatorLink.sharedKey) != string(responderLink.sharedKey) { t.Error("Shared keys do not match after full handshake") } if string(initiatorLink.derivedKey) != string(responderLink.derivedKey) { t.Error("Derived keys do not match after full handshake") } t.Logf("Full link establishment successful") t.Logf("Link ID: %x", initiatorLink.linkID) t.Logf("Shared key length: %d", len(initiatorLink.sharedKey)) t.Logf("Derived key length: %d", len(initiatorLink.derivedKey)) t.Logf("RTT: %.3f seconds", initiatorLink.rtt) }