libertaria-stack/core/l1-identity/proof_of_path.zig

335 lines
12 KiB
Zig

//! Proof of Path (RFC-0120)
//!
//! "Don't scan the graph. Prove the path."
//!
//! Sender includes O(depth) proof: [Sender->A, A->B, B->Receiver]
//! Receiver verifies in O(depth) checking only adjacent signatures.
//!
//! Enables Kenya-class devices to participate in the trust graph without having a huge database.
//!
//! Wire Format (CBOR-like structure):
//! [
//! hops: [[32]u8], // List of DIDs in chain
//! signatures: [[64]u8], // Sigs verifying links
//! timestamp: u64, // Creation time (replay protection)
//! expires_at: u64 // Path expiration
//! ]
const std = @import("std");
const trust_graph = @import("trust_graph");
const time = @import("time");
const soulkey = @import("soulkey");
pub const PathVerdict = enum {
/// Path is valid and active
valid,
/// Path explicitly starts/ends with wrong DIDs
invalid_endpoints,
/// Path expired
expired,
/// Path exceeds max trust depth (3 by default)
too_deep,
/// A link in the chain is broken (sig failure)
broken_link,
/// Signer revoked the trust edge
revoked,
/// Use for replay attacks
replay,
};
/// Proof of Path structure
/// "I am Sender. Here is a chain of signatures proving I am trusted by you."
pub const ProofOfPath = struct {
/// The trust chain: [Sender, Hop1, Hop2, ..., Receiver]
hops: std.ArrayListUnmanaged([32]u8),
/// Signatures proving each link:
/// signatures[i] = Sig_{hops[i+1]}(hops[i] + CONTEXT)
/// The receiver signs for Hop N-1, Hop N-1 signs for N-2...
/// NOTE: RFC-0120 implies Trust Edges are signed credentials.
/// Implementation: TrustEdge struct in graph is implicit proof.
/// This struct carries the *signatures* of those TrustEdges if they are signed.
/// For QVL v1 (local graph), PoP is a path reconstruction from local state or
/// a transmitted bundle of Signed Trust Edges.
///
/// REVISION for v1:
/// Since we use CompactTrustGraph (local state), PoP is primarily for *exporting*
/// a path to a receiver who *doesn't* know the sender.
/// The signatures here must be:
/// Link A->B: "I, B, trust A" (Signed by B)
signatures: std.ArrayListUnmanaged([64]u8),
/// Timestamp path was generated
timestamp: time.SovereignTimestamp,
/// When this proof expires (min of all edge expirations)
expires_at: time.SovereignTimestamp,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) ProofOfPath {
return .{
.hops = .{},
.signatures = .{},
.timestamp = time.SovereignTimestamp.now(), // Default, update later
.expires_at = time.SovereignTimestamp.now().addSeconds(3600), // Default 1h
.allocator = allocator,
};
}
pub fn deinit(self: *ProofOfPath) void {
self.hops.deinit(self.allocator);
self.signatures.deinit(self.allocator);
}
/// Construct a ProofOfPath (Sender Side)
/// Finds path in local graph and bundles it.
/// NOTE: In v1, we assume we have the signatures or can generate them if we own the keys.
/// Realistically, Sender constructs path from [Sender -> ... -> Receiver]
/// But wait, Trust flows Receiver -> Sender ("Receiver trusts Sender").
/// So the path is [Receiver -> A -> B -> Sender].
/// Sender needs to find: "Who does Receiver trust? A. Do I know A? No. Do I know B who knows A?"
///
/// RFC-0120 S4.3.3: "Sender constructs path"
/// This implies Sender knows the Trust Graph topology.
/// If Graph is Private, Sender *cannot* know Receiver's trustees.
///
/// RESOLUTION: PoP works on *Public/Friends* edges or previously exchanged credentials.
/// For v1 simulation: We assume Sender has a view of the graph that allows finding the path.
pub fn construct(
allocator: std.mem.Allocator,
sender_did: [32]u8,
receiver_did: [32]u8,
graph: *const trust_graph.CompactTrustGraph,
) !?ProofOfPath {
// Direction of Trust: Receiver -> ... -> Sender
// Sender needs to prove: "Receiver trusts X, X trusts Y, Y trusts ME."
// So we look for path: Receiver -> Sender
const path_indices = graph.findPath(receiver_did, sender_did) orelse return null;
defer allocator.free(path_indices);
var pop = ProofOfPath.init(allocator); // Default timestamp/expire
// Convert indices to DIDs
// Path: [Receiver(IDX), Hop1(IDX), ..., Sender(IDX)]
for (path_indices) |idx| {
const did = graph.getDid(idx) orelse return error.NodeNotFound;
try pop.hops.append(allocator, did);
}
// TODO: Retrieve specific edge signatures.
// For v1, we mock signatures or omit if relying on local graph verification.
// If the checking node HAS the graph (Chapter mode), it just calls verifyLocal(path).
// If transmitting to a stranger, we need actual crypto sigs.
// We will implement `signatures` placeholders for now.
// Fill mock signatures for structure validity
const sig_count = if (path_indices.len > 0) path_indices.len - 1 else 0;
for (0..sig_count) |_| {
var sig: [64]u8 = undefined;
@memset(&sig, 0xEE); // Mock sig
try pop.signatures.append(allocator, sig);
}
return pop;
}
/// Verify a received path against local Trust Graph (Receiver Side)
/// "Did the Sender provide a valid path that I can verify locally?"
/// Complexity: O(depth) - we just check the hops exist and link up.
pub fn verify(
self: *const ProofOfPath,
expected_receiver: [32]u8,
expected_sender: [32]u8,
graph: *const trust_graph.CompactTrustGraph,
) PathVerdict {
if (self.hops.items.len < 2) return .invalid_endpoints;
// 1. Verify Endpoints
// Hops[0] should be Receiver (Trust Anchor)
// Hops[Last] should be Sender (Trust Target)
// Direction: Receiver -> A -> B -> Sender
if (!std.mem.eql(u8, &self.hops.items[0], &expected_receiver)) return .invalid_endpoints;
if (!std.mem.eql(u8, &self.hops.items[self.hops.items.len - 1], &expected_sender)) return .invalid_endpoints;
// 2. Verify Expiration
if (self.expires_at.isBefore(time.SovereignTimestamp.now())) return .expired;
// 3. Verify Depth
if (self.hops.items.len - 1 > graph.config.max_trust_depth) return .too_deep;
// 4. Verify Links (O(Depth))
// We walk the path provided by Sender and check if our Local Graph agrees with the edges.
// (Or verify signatures if we implemented full credential verification logic)
var i: usize = 0;
while (i < self.hops.items.len - 1) : (i += 1) {
const truster_did = self.hops.items[i];
const trustee_did = self.hops.items[i + 1];
// Check if Truster -> Trustee exists in our view of the graph
// Ideally, we verify the SIGNATURE here.
// For v1 Local/Chapter verification:
if (!graph.hasDirectTrustByDid(truster_did, trustee_did)) {
return .broken_link;
}
}
return .valid;
}
/// Serialize to wire byte array (simple encoding)
pub fn serialize(self: *const ProofOfPath, allocator: std.mem.Allocator) ![]u8 {
var list = std.ArrayListUnmanaged(u8){};
defer list.deinit(allocator);
const writer = list.writer(allocator);
// Count (u8)
try writer.writeInt(u8, @intCast(self.hops.items.len), .little);
// Hops (32 bytes each)
for (self.hops.items) |hop| {
try writer.writeAll(&hop);
}
// Sigs (64 bytes each)
try writer.writeInt(u8, @intCast(self.signatures.items.len), .little);
for (self.signatures.items) |sig| {
try writer.writeAll(&sig);
}
// Times (17 bytes each)
try writer.writeAll(&self.timestamp.serialize());
try writer.writeAll(&self.expires_at.serialize());
return list.toOwnedSlice(allocator);
}
/// Deserialize from wire bytes
pub fn deserialize(allocator: std.mem.Allocator, data: []const u8) !ProofOfPath {
if (data.len < 1) return error.InvalidData;
var fbs = std.io.fixedBufferStream(data);
const reader = fbs.reader();
var pop = ProofOfPath.init(allocator);
// Hops
const hop_count = try reader.readInt(u8, .little);
for (0..hop_count) |_| {
var hop: [32]u8 = undefined;
try reader.readNoEof(&hop);
try pop.hops.append(allocator, hop);
}
// Sigs
const sig_count = try reader.readInt(u8, .little);
for (0..sig_count) |_| {
var sig: [64]u8 = undefined;
try reader.readNoEof(&sig);
try pop.signatures.append(allocator, sig);
}
// Times
var ts_buf: [17]u8 = undefined;
try reader.readNoEof(&ts_buf);
pop.timestamp = time.SovereignTimestamp.deserialize(&ts_buf);
try reader.readNoEof(&ts_buf);
pop.expires_at = time.SovereignTimestamp.deserialize(&ts_buf);
return pop;
}
};
// ============================================================================
// TESTS
// ============================================================================
test "ProofOfPath: construction and verification (valid flow)" {
const allocator = std.testing.allocator;
// 1. Setup Graph: R -> A -> S (Receiver trusts A, A trusts Sender)
// Receiver needs to verify S is trustworthy.
var r_did: [32]u8 = undefined;
@memset(&r_did, 0x11); // Receiver
var a_did: [32]u8 = undefined;
@memset(&a_did, 0xAA); // Intermediary
var s_did: [32]u8 = undefined;
@memset(&s_did, 0x99); // Sender
var graph = try trust_graph.CompactTrustGraph.init(allocator, r_did, .{});
defer graph.deinit();
// R trusts A
try graph.grantTrust(a_did, .full, .friends, 0);
// Manual edge A -> S (simulate A's trust)
const a_idx = graph.getNode(a_did).?;
const s_idx = try graph.getOrInsertNode(s_did);
try graph.adjacency.items[a_idx].append(allocator, .{ .target_idx = s_idx, .level = .full, .visibility = .public, .expires_at = 0 });
// 2. Sender constructs proof
var pop = try ProofOfPath.construct(allocator, s_did, r_did, &graph);
try std.testing.expect(pop != null);
defer if (pop) |*p| p.deinit();
// 3. Verify path contents
try std.testing.expectEqual(@as(usize, 3), pop.?.hops.items.len); // R, A, S
try std.testing.expectEqualSlices(u8, &r_did, &pop.?.hops.items[0]);
try std.testing.expectEqualSlices(u8, &s_did, &pop.?.hops.items[2]);
// 4. Receiver Validates
const verdict = pop.?.verify(r_did, s_did, &graph);
try std.testing.expectEqual(PathVerdict.valid, verdict);
}
test "ProofOfPath: verify broken link" {
const allocator = std.testing.allocator;
var r_did: [32]u8 = undefined;
@memset(&r_did, 0x11);
var a_did: [32]u8 = undefined;
@memset(&a_did, 0x22);
var s_did: [32]u8 = undefined;
@memset(&s_did, 0x33);
var graph = try trust_graph.CompactTrustGraph.init(allocator, r_did, .{});
defer graph.deinit();
// R trusts A
try graph.grantTrust(a_did, .full, .friends, 0);
// A doesn't trust S in the graph!
// Create fake PoP: R->A->S
var pop = ProofOfPath.init(allocator);
defer pop.deinit();
try pop.hops.append(allocator, r_did);
try pop.hops.append(allocator, a_did);
try pop.hops.append(allocator, s_did);
const verdict = pop.verify(r_did, s_did, &graph);
try std.testing.expectEqual(PathVerdict.broken_link, verdict);
}
test "ProofOfPath: serialization roundtrip" {
const allocator = std.testing.allocator;
var pop = ProofOfPath.init(allocator);
defer pop.deinit();
try pop.hops.append(allocator, [_]u8{1} ** 32);
try pop.hops.append(allocator, [_]u8{2} ** 32);
try pop.signatures.append(allocator, [_]u8{9} ** 64);
const serialized = try pop.serialize(allocator);
defer allocator.free(serialized);
var restored = try ProofOfPath.deserialize(allocator, serialized);
defer restored.deinit();
try std.testing.expectEqual(pop.hops.items.len, restored.hops.items.len);
try std.testing.expectEqualSlices(u8, &pop.hops.items[0], &restored.hops.items[0]);
try std.testing.expectEqual(pop.signatures.items.len, restored.signatures.items.len);
}