197 lines
5.2 KiB
Go
197 lines
5.2 KiB
Go
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/libp2p/go-libp2p"
|
|
|
|
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
|
|
)
|
|
|
|
// minimalHost creates a bare libp2p host for unit tests. No DHT, no relay.
|
|
func minimalHost(t *testing.T) (*Node, context.Context, context.CancelFunc) {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
|
|
if err != nil {
|
|
t.Fatalf("create host: %v", err)
|
|
}
|
|
t.Cleanup(func() { h.Close(); cancel() })
|
|
return &Node{Host: h, ctx: ctx, cancel: func() {}}, ctx, cancel
|
|
}
|
|
|
|
func TestCreateEnvelopeStoresToken(t *testing.T) {
|
|
op, err := cryptography.GenerateOperatorKey()
|
|
if err != nil {
|
|
t.Fatalf("generate operator key: %v", err)
|
|
}
|
|
n, ctx, _ := minimalHost(t)
|
|
_ = ctx
|
|
m := NewOperatorMessenger(ctx, n, op)
|
|
m.SetAuthToken(op.AuthToken)
|
|
env := m.CreateEnvelope(MsgTypePs, []byte("test"))
|
|
|
|
if len(env.Token) != 32 {
|
|
t.Fatalf("token missing from envelope: got %d bytes, want 32", len(env.Token))
|
|
}
|
|
if !bytes.Equal(env.Token, op.AuthToken) {
|
|
t.Fatal("envelope token != operator auth token")
|
|
}
|
|
if env.ID == 0 {
|
|
t.Fatal("envelope ID is zero — random generation failed")
|
|
}
|
|
}
|
|
|
|
func TestReplayProtection(t *testing.T) {
|
|
op, err := cryptography.GenerateOperatorKey()
|
|
if err != nil {
|
|
t.Fatalf("generate operator key: %v", err)
|
|
}
|
|
n, ctx, _ := minimalHost(t)
|
|
m := NewOperatorMessenger(ctx, n, op)
|
|
|
|
env1 := m.CreateEnvelope(MsgTypePs, []byte("test"))
|
|
env2 := m.CreateEnvelope(MsgTypePs, []byte("test"))
|
|
env2.ID = env1.ID // force same ID
|
|
|
|
if m.IsReplay(env1.ID) {
|
|
t.Fatal("first envelope flagged as replay")
|
|
}
|
|
if !m.IsReplay(env1.ID) {
|
|
t.Fatal("same ID not detected as replay on second check")
|
|
}
|
|
if !m.IsReplay(env2.ID) {
|
|
t.Fatal("replay envelope with same ID not detected")
|
|
}
|
|
}
|
|
|
|
func TestAuthTokenRejection(t *testing.T) {
|
|
op, err := cryptography.GenerateOperatorKey()
|
|
if err != nil {
|
|
t.Fatalf("generate operator key: %v", err)
|
|
}
|
|
n, ctx, _ := minimalHost(t)
|
|
m := NewOperatorMessenger(ctx, n, op)
|
|
m.SetAuthToken(op.AuthToken)
|
|
|
|
env := m.CreateEnvelope(MsgTypePs, []byte("test"))
|
|
if len(env.Token) != 32 {
|
|
t.Fatalf("token not set: got %d bytes", len(env.Token))
|
|
}
|
|
|
|
// Simulate implant-side check: wrong token should be rejected
|
|
wrongToken := make([]byte, 32)
|
|
for i := range wrongToken {
|
|
wrongToken[i] = 0xFF
|
|
}
|
|
if bytes.Equal(env.Token, wrongToken) {
|
|
t.Fatal("tampered token accidentally matches")
|
|
}
|
|
if bytes.Equal(env.Token, op.AuthToken) {
|
|
// expected — token is correct. Now verify wrong token fails
|
|
env.Token = wrongToken
|
|
if bytes.Equal(env.Token, op.AuthToken) {
|
|
t.Fatal("failed to set wrong token")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnvelopeSignAndVerify(t *testing.T) {
|
|
op, err := cryptography.GenerateOperatorKey()
|
|
if err != nil {
|
|
t.Fatalf("generate operator key: %v", err)
|
|
}
|
|
n, ctx, _ := minimalHost(t)
|
|
m := NewOperatorMessenger(ctx, n, op)
|
|
|
|
data := []byte("necropolis command data")
|
|
env := m.CreateEnvelope(MsgTypeExecute, data)
|
|
|
|
signingData, err := EnvelopeSigningBytes(env)
|
|
if err != nil {
|
|
t.Fatalf("signing bytes: %v", err)
|
|
}
|
|
sig, err := op.PrivateKey.Sign(signingData)
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
env.Signature = sig
|
|
|
|
ok, err := cryptography.Verify(op.PublicKey, signingData, sig)
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("signature verification failed")
|
|
}
|
|
|
|
// Tamper with data, re-signing bytes change, verify should fail
|
|
data[0] ^= 0xFF
|
|
env.Data = data
|
|
signingData2, _ := EnvelopeSigningBytes(env)
|
|
ok2, _ := cryptography.Verify(op.PublicKey, signingData2, sig)
|
|
if ok2 {
|
|
t.Fatal("tampered data passed signature verification")
|
|
}
|
|
}
|
|
|
|
func TestTopicFormats(t *testing.T) {
|
|
op, err := cryptography.GenerateOperatorKey()
|
|
if err != nil {
|
|
t.Fatalf("generate operator key: %v", err)
|
|
}
|
|
n, ctx, _ := minimalHost(t)
|
|
m := NewOperatorMessenger(ctx, n, op)
|
|
m.SetOperatorID(op.PeerID)
|
|
opID := op.PeerID.String()
|
|
|
|
if m.BeaconTopic() != "/b/"+opID+"/bx" {
|
|
t.Fatalf("beacon topic: got %s", m.BeaconTopic())
|
|
}
|
|
if m.CommandTopic() != "/c/"+opID+"/cx" {
|
|
t.Fatalf("command topic: got %s", m.CommandTopic())
|
|
}
|
|
taskTopic := m.TaskTopic("test-implant-id")
|
|
if !strings.HasPrefix(taskTopic, "/b/") || !strings.HasSuffix(taskTopic, "/tx/test-implant-id") {
|
|
t.Fatalf("task topic: got %s", taskTopic)
|
|
}
|
|
}
|
|
|
|
func TestBoxEncryptDecryptViaEnvelope(t *testing.T) {
|
|
op, err := cryptography.GenerateOperatorKey()
|
|
if err != nil {
|
|
t.Fatalf("generate operator key: %v", err)
|
|
}
|
|
n, ctx, _ := minimalHost(t)
|
|
m := NewOperatorMessenger(ctx, n, op)
|
|
|
|
plain := []byte("secret implant command")
|
|
env := m.CreateEnvelope(MsgTypeExecute, plain)
|
|
|
|
// Encrypt the data field
|
|
if op.BoxKeys == nil {
|
|
t.Skip("no box keys")
|
|
}
|
|
encrypted, err := cryptography.EncryptMessage(env.Data, op.BoxKeys.PublicKey)
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
if bytes.Equal(encrypted, plain) {
|
|
t.Fatal("encrypted data equals plaintext")
|
|
}
|
|
env.Data = encrypted
|
|
|
|
// Decrypt
|
|
decrypted, err := cryptography.DecryptMessage(env.Data, op.BoxKeys)
|
|
if err != nil {
|
|
t.Fatalf("decrypt: %v", err)
|
|
}
|
|
if !bytes.Equal(decrypted, plain) {
|
|
t.Fatalf("decrypted != plain: got %q, want %q", decrypted, plain)
|
|
}
|
|
}
|