feat(crypto): Integrate ECDH & XChaCha20-Poly1305 for Relay
Phase 14 Real Crypto Update: - Replaced mock encryption with XChaCha20-Poly1305 + X25519 ECDH. - Implemented strict Nonce/SessionID binding (RFC-0000 alignment). - Updated RelayPacket wire format to include Ephemeral Key. - Updated RelayService to unwrap using Node Identity (SoulKey). - Extended DHT and Federation protocols to propagate X25519 Public Keys. - Persisted peer keys in SQLite storage. - Tests passing (including new crypto logic).
This commit is contained in:
parent
fca9ac13e0
commit
e5f59869bc
|
|
@ -11,6 +11,7 @@ pub const SERVICE_TYPE: u16 = lwf.LWFHeader.ServiceType.IDENTITY_SIGNAL;
|
|||
pub const DhtNode = struct {
|
||||
id: [32]u8,
|
||||
address: net.Address,
|
||||
key: [32]u8,
|
||||
};
|
||||
|
||||
pub const SessionState = enum {
|
||||
|
|
@ -79,6 +80,7 @@ pub const FederationMessage = union(enum) {
|
|||
try writer.writeInt(u16, @intCast(n.nodes.len), .big);
|
||||
for (n.nodes) |node| {
|
||||
try writer.writeAll(&node.id);
|
||||
try writer.writeAll(&node.key);
|
||||
// For now we only support IPv4 in DHT nodes responses
|
||||
if (node.address.any.family == std.posix.AF.INET) {
|
||||
try writer.writeAll(&std.mem.toBytes(node.address.in.sa.addr));
|
||||
|
|
@ -143,11 +145,13 @@ pub const FederationMessage = union(enum) {
|
|||
const nodes = try allocator.alloc(DhtNode, count);
|
||||
for (0..count) |i| {
|
||||
const id = try reader.readBytesNoEof(32);
|
||||
const key = try reader.readBytesNoEof(32);
|
||||
const addr_u32 = try reader.readInt(u32, @import("builtin").target.cpu.arch.endian());
|
||||
const port = try reader.readInt(u16, .big);
|
||||
nodes[i] = .{
|
||||
.id = id,
|
||||
.address = net.Address.initIp4(std.mem.toBytes(addr_u32), port),
|
||||
.key = key,
|
||||
};
|
||||
}
|
||||
return .{ .dht_nodes = .{ .nodes = nodes } };
|
||||
|
|
|
|||
|
|
@ -27,19 +27,7 @@ const relay_service_mod = @import("relay_service.zig");
|
|||
const NodeConfig = config_mod.NodeConfig;
|
||||
const UTCP = utcp_mod.UTCP;
|
||||
// SoulKey definition (temporarily embedded until module is available)
|
||||
const SoulKey = struct {
|
||||
did: [32]u8,
|
||||
public_key: [32]u8,
|
||||
|
||||
pub fn fromSeed(seed: *const [32]u8) !SoulKey {
|
||||
var public_key: [32]u8 = undefined;
|
||||
std.crypto.hash.sha2.Sha256.hash(seed, &public_key, .{});
|
||||
return SoulKey{
|
||||
.did = public_key,
|
||||
.public_key = public_key,
|
||||
};
|
||||
}
|
||||
};
|
||||
const SoulKey = l1.SoulKey;
|
||||
const RiskGraph = qvl.types.RiskGraph;
|
||||
const DiscoveryService = discovery_mod.DiscoveryService;
|
||||
const PeerTable = peer_table_mod.PeerTable;
|
||||
|
|
@ -289,15 +277,12 @@ pub const CapsuleNode = struct {
|
|||
|
||||
if (frame.header.service_type == fed.SERVICE_TYPE) {
|
||||
try self.handleFederationMessage(result.sender, frame);
|
||||
} else if (frame.header.service_type == l0.LWFHeader.ServiceType.RELAY_FORWARD) {
|
||||
// Phase 14: Relay Forwarding
|
||||
if (self.relay_service) |*rs| {
|
||||
std.log.debug("Relay: Received relay packet from {f}", .{result.sender});
|
||||
// Mock secret for now (needs ECDH)
|
||||
const shared_secret = [_]u8{0xAA} ** 32;
|
||||
|
||||
// Unwrap and forward
|
||||
if (rs.forwardPacket(frame.payload, shared_secret)) |next_hop_data| {
|
||||
// Unwrap and forward using our private key (as receiver)
|
||||
if (rs.forwardPacket(frame.payload, self.identity.x25519_private)) |next_hop_data| {
|
||||
// next_hop_data.payload is now the INNER payload
|
||||
const next_node_id = next_hop_data.next_hop;
|
||||
|
||||
|
|
@ -305,6 +290,8 @@ pub const CapsuleNode = struct {
|
|||
// TODO: Check if we are final destination (all zeros) handled by forwardPacket
|
||||
// But forwardPacket returns the result to US to send.
|
||||
|
||||
// Check if we are destination handled by forwardPacket via null next_hop logic?
|
||||
// forwardPacket returns next_hop. If all zeros, it means LOCAL delivery.
|
||||
var is_final = true;
|
||||
for (next_node_id) |b| {
|
||||
if (b != 0) {
|
||||
|
|
@ -314,16 +301,18 @@ pub const CapsuleNode = struct {
|
|||
}
|
||||
|
||||
if (is_final) {
|
||||
// We are the destination!
|
||||
// TODO: Process inner payload as a new frame
|
||||
std.log.info("Relay: Final Packet Received! Size: {d}", .{next_hop_data.payload.len});
|
||||
// Final delivery to US
|
||||
std.log.info("Relay: Final Packet Received for Session {x}! Payload Size: {d}", .{ next_hop_data.session_id, next_hop_data.payload.len });
|
||||
// TODO: Hand over payload to upper layers (e.g. Chat/Protocol handler)
|
||||
// For MVP, just log.
|
||||
} else {
|
||||
// Forward to next hop
|
||||
// Need to lookup IP for next_node_id
|
||||
// Lookup IP
|
||||
const next_remote = self.dht.routing_table.findNode(next_node_id);
|
||||
if (next_remote) |remote| {
|
||||
// Re-wrap in LWF for transport
|
||||
try self.utcp.send(remote.address, next_hop_data.payload, l0.LWFHeader.ServiceType.RELAY_FORWARD);
|
||||
std.log.info("Relay: Forwarded packet to {f}", .{remote.address});
|
||||
std.log.info("Relay: Forwarded packet to {f} (Session {x})", .{ remote.address, next_hop_data.session_id });
|
||||
} else {
|
||||
std.log.warn("Relay: Next hop {x} not found in routing table", .{next_node_id[0..4]});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,21 @@ pub const RelayService = struct {
|
|||
_ = self;
|
||||
}
|
||||
|
||||
/// Forward a relay packet to the next hop
|
||||
/// Returns the next hop address and the inner payload
|
||||
/// Forward a relay packet to the next hop
|
||||
/// Returns the next hop address and the inner payload
|
||||
pub fn forwardPacket(
|
||||
self: *RelayService,
|
||||
packet: relay_mod.RelayPacket,
|
||||
shared_secret: [32]u8,
|
||||
) !struct { next_hop: [32]u8, payload: []u8 } {
|
||||
// Unwrap the onion layer
|
||||
const result = try self.onion_builder.unwrapLayer(packet, shared_secret);
|
||||
raw_packet: []const u8,
|
||||
receiver_private_key: [32]u8,
|
||||
) !struct { next_hop: [32]u8, payload: []u8, session_id: [16]u8 } {
|
||||
// Parse the wire packet
|
||||
var packet = try relay_mod.RelayPacket.decode(self.allocator, raw_packet);
|
||||
defer packet.deinit(self.allocator);
|
||||
|
||||
// Unwrap the onion layer (using our private key + packet's ephemeral key)
|
||||
const result = try self.onion_builder.unwrapLayer(packet, receiver_private_key, null);
|
||||
|
||||
// Check if next_hop is all zeros (meaning we're the final destination)
|
||||
const is_final = blk: {
|
||||
|
|
@ -48,15 +54,19 @@ pub const RelayService = struct {
|
|||
|
||||
if (is_final) {
|
||||
// We're the final destination - deliver locally
|
||||
std.log.info("Relay: Final destination reached, delivering payload locally", .{});
|
||||
std.log.info("Relay: Final destination reached for session {x}", .{result.session_id});
|
||||
self.packets_dropped += 1; // Not actually dropped, just not forwarded
|
||||
return result;
|
||||
}
|
||||
|
||||
// Forward to next hop
|
||||
std.log.debug("Relay: Forwarding to next hop: {x}", .{std.fmt.fmtSliceHexLower(&result.next_hop)});
|
||||
std.log.debug("Relay: Forwarding session {x} to next hop: {x}", .{ result.session_id, std.fmt.fmtSliceHexLower(&result.next_hop) });
|
||||
self.packets_forwarded += 1;
|
||||
|
||||
// Result payload includes the re-wrapped inner onion?
|
||||
// Wait, unwrapLayer returns the decrypted payload.
|
||||
// In onion routing, the decrypted payload IS the inner onion for the next hop.
|
||||
// We just return it. The caller (node.zig) must send it.
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ pub const StorageService = struct {
|
|||
\\ id BLOB PRIMARY KEY,
|
||||
\\ address TEXT NOT NULL,
|
||||
\\ last_seen INTEGER NOT NULL,
|
||||
\\ seen_count INTEGER DEFAULT 1
|
||||
\\ seen_count INTEGER DEFAULT 1,
|
||||
\\ x25519_key BLOB
|
||||
\\ );
|
||||
\\ CREATE TABLE IF NOT EXISTS qvl_nodes (
|
||||
\\ did BLOB PRIMARY KEY,
|
||||
|
|
@ -84,8 +85,8 @@ pub const StorageService = struct {
|
|||
}
|
||||
|
||||
pub fn savePeer(self: *StorageService, node: RemoteNode) !void {
|
||||
const sql = "INSERT INTO peers (id, address, last_seen) VALUES (?, ?, ?) " ++
|
||||
"ON CONFLICT(id) DO UPDATE SET address=excluded.address, last_seen=excluded.last_seen, seen_count=seen_count+1;";
|
||||
const sql = "INSERT INTO peers (id, address, last_seen, x25519_key) VALUES (?, ?, ?, ?) " ++
|
||||
"ON CONFLICT(id) DO UPDATE SET address=excluded.address, last_seen=excluded.last_seen, seen_count=seen_count+1, x25519_key=excluded.x25519_key;";
|
||||
|
||||
var stmt: ?*c.sqlite3_stmt = null;
|
||||
if (c.sqlite3_prepare_v2(self.db, sql, -1, &stmt, null) != c.SQLITE_OK) return error.PrepareFailed;
|
||||
|
|
@ -102,11 +103,14 @@ pub const StorageService = struct {
|
|||
// Bind Last Seen
|
||||
_ = c.sqlite3_bind_int64(stmt, 3, node.last_seen);
|
||||
|
||||
// Bind Key
|
||||
_ = c.sqlite3_bind_blob(stmt, 4, &node.key, 32, null);
|
||||
|
||||
if (c.sqlite3_step(stmt) != c.SQLITE_DONE) return error.StepFailed;
|
||||
}
|
||||
|
||||
pub fn loadPeers(self: *StorageService, allocator: std.mem.Allocator) ![]RemoteNode {
|
||||
const sql = "SELECT id, address, last_seen FROM peers;";
|
||||
const sql = "SELECT id, address, last_seen, x25519_key FROM peers;";
|
||||
var stmt: ?*c.sqlite3_stmt = null;
|
||||
if (c.sqlite3_prepare_v2(self.db, sql, -1, &stmt, null) != c.SQLITE_OK) return error.PrepareFailed;
|
||||
defer _ = c.sqlite3_finalize(stmt);
|
||||
|
|
@ -119,6 +123,8 @@ pub const StorageService = struct {
|
|||
const id_len = c.sqlite3_column_bytes(stmt, 0);
|
||||
const addr_ptr = c.sqlite3_column_text(stmt, 1);
|
||||
const last_seen = c.sqlite3_column_int64(stmt, 2);
|
||||
const key_ptr = c.sqlite3_column_blob(stmt, 3);
|
||||
const key_len = c.sqlite3_column_bytes(stmt, 3);
|
||||
|
||||
if (id_len != ID_LEN) continue;
|
||||
|
||||
|
|
@ -128,6 +134,11 @@ pub const StorageService = struct {
|
|||
const addr_str = std.mem.span(addr_ptr);
|
||||
node.address = try std.net.Address.parseIp(addr_str, 0); // Port logic handled via federation later
|
||||
node.last_seen = last_seen;
|
||||
if (key_len == 32) {
|
||||
@memcpy(&node.key, @as([*]const u8, @ptrCast(key_ptr))[0..32]);
|
||||
} else {
|
||||
@memset(&node.key, 0);
|
||||
}
|
||||
|
||||
try list.append(allocator, node);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ pub const RemoteNode = struct {
|
|||
id: NodeId,
|
||||
address: net.Address,
|
||||
last_seen: i64,
|
||||
key: [32]u8 = [_]u8{0} ** 32, // X25519 Public Key
|
||||
};
|
||||
|
||||
pub const KBucket = struct {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
//! - Fixed-size trailer (36 bytes)
|
||||
//! - Checksum verification (CRC32-C)
|
||||
//! - Signature support (Ed25519)
|
||||
//! - Nonce/SessionID Binding:
|
||||
//! Cryptography nonce construction MUST strictly bind to the Session ID.
|
||||
//! Usage: `nonce[0..16] == session_id`, `nonce[16..24] == random/counter`.
|
||||
//!
|
||||
//! Frame structure:
|
||||
//! ┌──────────────────┐
|
||||
|
|
|
|||
|
|
@ -33,15 +33,14 @@ pub const NextHopHeader = struct {
|
|||
/// It effectively contains an encrypted blob that the receiver can decrypt
|
||||
/// to reveal the NextHopHeader and the inner Payload.
|
||||
pub const RelayPacket = struct {
|
||||
// Public ephemeral key for ECDH could be here if we do per-packet keying,
|
||||
// but typically we use established session keys or pre-keys.
|
||||
// For simplicity V1, we assume a session key exists or use a nonce.
|
||||
|
||||
nonce: [24]u8, // XChaCha20 nonce
|
||||
// X25519 Public Key for ephemeral key agreement
|
||||
ephemeral_key: [32]u8,
|
||||
nonce: [24]u8, // XChaCha20 nonce (SessionID + Random)
|
||||
ciphertext: []u8, // Encrypted [NextHopHeader + InnerPayload]
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, size: usize) !RelayPacket {
|
||||
return RelayPacket{
|
||||
.ephemeral_key = undefined,
|
||||
.nonce = undefined, // To be filled
|
||||
.ciphertext = try allocator.alloc(u8, size),
|
||||
};
|
||||
|
|
@ -50,6 +49,31 @@ pub const RelayPacket = struct {
|
|||
pub fn deinit(self: *RelayPacket, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.ciphertext);
|
||||
}
|
||||
|
||||
/// Serialize to wire format
|
||||
pub fn encode(self: *const RelayPacket, allocator: std.mem.Allocator) ![]u8 {
|
||||
const total_size = 32 + 24 + self.ciphertext.len;
|
||||
var buf = try allocator.alloc(u8, total_size);
|
||||
|
||||
@memcpy(buf[0..32], &self.ephemeral_key);
|
||||
@memcpy(buf[32..56], &self.nonce);
|
||||
@memcpy(buf[56..], self.ciphertext);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Deserialize from wire format
|
||||
pub fn decode(allocator: std.mem.Allocator, data: []const u8) !RelayPacket {
|
||||
if (data.len < 32 + 24) return error.PacketTooSmall;
|
||||
const ciphertext_len = data.len - 32 - 24;
|
||||
|
||||
var packet = try RelayPacket.init(allocator, ciphertext_len);
|
||||
@memcpy(&packet.ephemeral_key, data[0..32]);
|
||||
@memcpy(&packet.nonce, data[32..56]);
|
||||
@memcpy(packet.ciphertext, data[56..]);
|
||||
|
||||
return packet;
|
||||
}
|
||||
};
|
||||
|
||||
/// Logic to construct an onion packet.
|
||||
|
|
@ -64,13 +88,19 @@ pub const OnionBuilder = struct {
|
|||
|
||||
/// Wraps a payload into a single layer of encryption for a specific relay.
|
||||
/// In a real onion, this is called iteratively from innermost to outermost.
|
||||
/// Uses ECDH with next_hop_pubkey to derive a shared secret.
|
||||
pub fn wrapLayer(
|
||||
self: *OnionBuilder,
|
||||
payload: []const u8,
|
||||
next_hop: [32]u8,
|
||||
shared_secret: [32]u8,
|
||||
next_hop_pubkey: [32]u8,
|
||||
session_id: [16]u8,
|
||||
) !RelayPacket {
|
||||
_ = shared_secret;
|
||||
// 1. Generate Ephemeral Keypair
|
||||
const kp = crypto.dh.X25519.KeyPair.generate();
|
||||
|
||||
// 2. Compute Shared Secret
|
||||
const shared_secret = try crypto.dh.X25519.scalarmult(kp.secret_key, next_hop_pubkey);
|
||||
// 1. Construct Cleartext: [NextHop (32) | Payload (N)]
|
||||
var cleartext = try self.allocator.alloc(u8, 32 + payload.len);
|
||||
defer self.allocator.free(cleartext);
|
||||
|
|
@ -78,40 +108,73 @@ pub const OnionBuilder = struct {
|
|||
@memcpy(cleartext[0..32], &next_hop);
|
||||
@memcpy(cleartext[32..], payload);
|
||||
|
||||
// 2. Encrypt
|
||||
var packet = try RelayPacket.init(self.allocator, cleartext.len + 16); // +AuthTag
|
||||
crypto.random.bytes(&packet.nonce);
|
||||
// 2. Encrypt using XChaCha20-Poly1305
|
||||
const tag_len = crypto.aead.chacha_poly.XChaCha20Poly1305.tag_length;
|
||||
|
||||
// Mock Encryption (XChaCha20-Poly1305 would go here)
|
||||
// For MVP structure, we just copy (TODO: Add actual crypto integration)
|
||||
// We simulate "encryption" by XORing with a byte for testing proving modification works
|
||||
for (cleartext, 0..) |b, i| {
|
||||
packet.ciphertext[i] = b ^ 0xFF; // Simple NOT for mock encryption
|
||||
}
|
||||
// Mock Auth Tag
|
||||
@memset(packet.ciphertext[cleartext.len..], 0xAA);
|
||||
var packet = try RelayPacket.init(self.allocator, cleartext.len + tag_len);
|
||||
|
||||
// Store Ephemeral Public Key in Packet
|
||||
@memcpy(&packet.ephemeral_key, &kp.public_key);
|
||||
|
||||
// Nonce Construction: SessionID (16) + Random (8)
|
||||
@memcpy(packet.nonce[0..16], &session_id);
|
||||
crypto.random.bytes(packet.nonce[16..24]);
|
||||
|
||||
var tag: [tag_len]u8 = undefined;
|
||||
crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(
|
||||
packet.ciphertext[0..cleartext.len],
|
||||
&tag,
|
||||
cleartext,
|
||||
"", // No associated data for now
|
||||
packet.nonce,
|
||||
shared_secret,
|
||||
);
|
||||
|
||||
// Append tag to ciphertext
|
||||
@memcpy(packet.ciphertext[cleartext.len..], &tag);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
/// Unwraps a single layer (Server/Relay side logic).
|
||||
/// Uses receiver_secret_key (node's private key) to derive shared secret from packet's ephemeral key.
|
||||
pub fn unwrapLayer(
|
||||
self: *OnionBuilder,
|
||||
packet: RelayPacket,
|
||||
shared_secret: [32]u8,
|
||||
) !struct { next_hop: [32]u8, payload: []u8 } {
|
||||
_ = shared_secret;
|
||||
receiver_secret_key: [32]u8,
|
||||
expected_session_id: ?[16]u8,
|
||||
) !struct { next_hop: [32]u8, payload: []u8, session_id: [16]u8 } {
|
||||
// 1. Compute Shared Secret from Ephemeral Key
|
||||
const shared_secret = crypto.dh.X25519.scalarmult(receiver_secret_key, packet.ephemeral_key) catch return error.DecryptionFailed;
|
||||
const tag_len = crypto.aead.chacha_poly.XChaCha20Poly1305.tag_length;
|
||||
if (packet.ciphertext.len < 32 + tag_len) return error.DecryptionFailed;
|
||||
|
||||
// Mock Decryption
|
||||
if (packet.ciphertext.len < 32 + 16) return error.DecryptionFailed;
|
||||
// Verify Session ID part of Nonce if provided
|
||||
var session_id: [16]u8 = undefined;
|
||||
@memcpy(&session_id, packet.nonce[0..16]);
|
||||
|
||||
const content_len = packet.ciphertext.len - 16;
|
||||
var cleartext = try self.allocator.alloc(u8, content_len);
|
||||
|
||||
for (0..content_len) |i| {
|
||||
cleartext[i] = packet.ciphertext[i] ^ 0xFF;
|
||||
if (expected_session_id) |expected| {
|
||||
if (!std.mem.eql(u8, &expected, &session_id)) {
|
||||
return error.DecryptionFailed; // Wrong session context
|
||||
}
|
||||
}
|
||||
|
||||
const content_len = packet.ciphertext.len - tag_len;
|
||||
var cleartext = try self.allocator.alloc(u8, content_len);
|
||||
defer self.allocator.free(cleartext); // Free after copy
|
||||
|
||||
var tag: [tag_len]u8 = undefined;
|
||||
@memcpy(&tag, packet.ciphertext[content_len..]);
|
||||
|
||||
try crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(
|
||||
cleartext,
|
||||
packet.ciphertext[0..content_len],
|
||||
tag,
|
||||
"", // Associated data
|
||||
packet.nonce,
|
||||
shared_secret,
|
||||
);
|
||||
|
||||
var next_hop: [32]u8 = undefined;
|
||||
@memcpy(&next_hop, cleartext[0..32]);
|
||||
|
||||
|
|
@ -120,11 +183,10 @@ pub const OnionBuilder = struct {
|
|||
const payload = try self.allocator.alloc(u8, payload_len);
|
||||
@memcpy(payload, cleartext[32..]);
|
||||
|
||||
self.allocator.free(cleartext);
|
||||
|
||||
return .{
|
||||
.next_hop = next_hop,
|
||||
.payload = payload,
|
||||
.session_id = session_id,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -135,17 +197,24 @@ test "Relay: wrap and unwrap" {
|
|||
|
||||
const payload = "Hello Onion!";
|
||||
const next_hop = [_]u8{0xAB} ** 32;
|
||||
const shared_secret = [_]u8{0} ** 32;
|
||||
// Generate valid KeyPair for testing
|
||||
const kp = crypto.dh.X25519.KeyPair.generate();
|
||||
const receiver_pubkey = kp.public_key;
|
||||
const receiver_seckey = kp.secret_key;
|
||||
|
||||
var packet = try builder.wrapLayer(payload, next_hop, shared_secret);
|
||||
const session_id = [_]u8{0xCC} ** 16;
|
||||
|
||||
var packet = try builder.wrapLayer(payload, next_hop, receiver_pubkey, session_id);
|
||||
defer packet.deinit(allocator);
|
||||
|
||||
// Verify it is "encrypted" (XOR 0xFF)
|
||||
// Payload "H" (0x48) ^ 0xFF = 0xB7
|
||||
// First byte of cleartext is next_hop[0] (0xAB) ^ 0xFF = 0x54
|
||||
try std.testing.expectEqual(@as(u8, 0x54), packet.ciphertext[0]);
|
||||
// Verify it is encrypted (not plain)
|
||||
// First byte of cleartext should NOT be next_hop[0] (0xAB)
|
||||
try std.testing.expect(packet.ciphertext[0] != 0xAB);
|
||||
|
||||
const result = try builder.unwrapLayer(packet, shared_secret);
|
||||
// Verify first 16 bytes of nonce are session_id
|
||||
try std.testing.expectEqualSlices(u8, &session_id, packet.nonce[0..16]);
|
||||
|
||||
const result = try builder.unwrapLayer(packet, receiver_seckey, session_id);
|
||||
defer allocator.free(result.payload);
|
||||
|
||||
try std.testing.expectEqualSlices(u8, &next_hop, &result.next_hop);
|
||||
|
|
|
|||
Loading…
Reference in New Issue