On Sun, 2025-05-11 at 04:01 +0200, KP Singh wrote: [...]
For this specific BPF case, we will directly sign a composite of the first message and the hash of the second. Let H_meta = H(M_metadata). The block to be signed is effectively:
B_signed = I_loader || H_meta
The signature generated is Sig(B_signed).
The process then follows a similar pattern to the Alice and Bob model, where the kernel (Bob) verifies I_loader and H_meta using the signature. Then, the trusted I_loader is responsible for verifying M_metadata against the trusted H_meta.
From an implementation standpoint:
# Build
bpftool (or some other tool in the user's build environment) knows about the metadata (M_metadata) and the loader program (I_loader). It first calculates H_meta = H(M_metadata). Then it constructs the object to be signed and computes the signature:
Sig(I_loader || H_meta)
# Loader
bpftool generates the loader program. The initial instructions of this loader program are designed to verify the SHA256 hash of the metadata (M_metadata) that will be passed in a map. These instructions effectively embed the precomputed H_meta as immediate values.
ld_imm64 r1, const_ptr_to_map // insn[0].src_reg == BPF_PSEUDO_MAP_IDX r2 = *(u64 *)(r1 + 0); ld_imm64 r3, sha256_of_map_part1 // constant precomputed by bpftool (part of H_meta) if r2 != r3 goto out;
r2 = *(u64 *)(r1 + 8); ld_imm64 r3, sha256_of_map_part2 // (part of H_meta) if r2 != r3 goto out;
r2 = *(u64 *)(r1 + 16); ld_imm64 r3, sha256_of_map_part3 // (part of H_meta) if r2 != r3 goto out;
r2 = *(u64 *)(r1 + 24); ld_imm64 r3, sha256_of_map_part4 // (part of H_meta) if r2 != r3 goto out; ...
This implicitly makes the payload equivalent to the signed block (B_signed)
I_loader || H_meta
bpftool then generates the signature of this I_loader payload (which now contains the expected H_meta) using a key (system or user) with new flags that work in combination with bpftool -L
Could I just push back a bit on this. The theory of hash chains (which I've cut to shorten) is about pure data structures. The reason for that is that the entire hash chain is supposed to be easily independently verifiable in any environment because anything can compute the hashes of the blocks and links. This independent verification of the chain is key to formally proving hash chains to be correct. In your proposal we lose the easy verifiability because the link hash is embedded in the ebpf loader program which has to be disassembled to do the extraction of the hash and verify the loader is actually checking it.
I was looking at ways we could use a pure hash chain (i.e. signature over loader and real map hash) and it does strike me that the above ebpf hash verification code is pretty invariant and easy to construct, so it could run as a separate BPF fragment that then jumps to the real loader. In that case, it could be constructed on the fly in a trusted environment, like the kernel, from the link hash in the signature and the signature could just be Sig(loader || map hash) which can then be easily verified without having to disassemble ebpf code. So we get the formal provability benefits of using a real hash chain while still keeping your verification in BPF.
Regards,
James