PQC-signed load gate for eBPF programs on AI inference servers. ML-DSA signatures, allow-list policy, audit log for every load attempt.
Project description
PQC eBPF Attestation
A post-quantum signed load gate for eBPF programs on AI inference servers. eBPF lets code run inside the Linux kernel; it is phenomenal for observability and security, and catastrophic as a supply-chain attack vector. A malicious eBPF program loaded on an inference host can silently intercept model calls, exfiltrate weights out of /dev/nvidia* handles, or rewrite output tokens on the way back to the user - with no trace at the application layer. This library is the cryptographic envelope that sits before bpf_prog_load(): every program is ML-DSA signed, every signer has a DID, every load attempt is matched against a LoadPolicy and appended to an audit log. The actual kernel integration (an LSM hook, a pre-load userspace verifier, or a Kubernetes admission controller) is the user's job; this library gives you the data structures, signing, verification, and policy engine to plug into that integration.
The Problem
AI inference servers are a uniquely sensitive target for eBPF-based attacks:
- Weight exfiltration. A
kprobeortracingprogram attached to a CUDA or cgroup memory path can read GPU-adjacent pages and stream out model weights over a socket. - Silent tampering. An
XDPorsched_clsprogram can rewrite outbound JSON from an inference API mid-flight. - Observability poisoning. A
perf_eventprogram can filter out its own footprint from the very telemetry the defender relies on. - Supply chain. eBPF programs shipped as
.bpf.oobjects by vendors, operators, or observability agents have no standard signing model today; the kernel happily loads anything withCAP_BPF.
Pre-quantum signatures (RSA, ECDSA, Ed25519) also carry a long-term forgeability risk: signed eBPF binaries retain trust for years, well into the timeline where a cryptographically relevant quantum computer could forge new ones retroactively.
The Solution
- ML-DSA (FIPS 204) signatures over a canonical manifest:
metadata + SHA3-256(bytecode) + size. The signature does not bloat with the bytecode. - DID-based signer identity via
quantumshield.identity.agent.AgentIdentity- every signer has a stabledid:pqaid:<hash>that policies can reason about. LoadPolicywith ordered rules - each rule covers a set ofBPFProgramTypes, an allow-list of signer DIDs, an optional signature requirement, and a max-size cap. First matching rule wins; no match falls through to a configurable default (deny by default).- Append-only
AttestationLog- every load attempt is recorded with its signer, hash, decision, reason, and actor, regardless of whether it was accepted. - CLI (
pqc-bpf sign | verify | info) for ops teams.
Installation
pip install pqc-ebpf-attestation
Development:
pip install -e ".[dev]"
Quick Start
from quantumshield.identity.agent import AgentIdentity
from pqc_ebpf_attestation import (
AttestationLog,
BPFProgram,
BPFProgramMetadata,
BPFProgramType,
BPFSigner,
BPFVerifier,
LoadPolicy,
PolicyRule,
)
# 1. Load and sign a compiled eBPF object.
metadata = BPFProgramMetadata(
name="trace_sys_enter_read",
program_type=BPFProgramType.KPROBE,
attach_point="sys_enter_read",
author="ops-team",
)
program = BPFProgram.from_file(metadata, "trace.bpf.o")
identity = AgentIdentity.create("bpf-signer", capabilities=["sign"])
signer = BPFSigner(identity)
signed = signer.sign(program)
# 2. Verify independently.
result = BPFVerifier.verify(signed)
assert result.valid
# 3. Enforce a policy at load time.
policy = LoadPolicy().add_rule(
PolicyRule(
program_types=(BPFProgramType.KPROBE, BPFProgramType.TRACING),
allowed_signers=frozenset({identity.did}),
)
)
log = AttestationLog()
decision, reason = policy.evaluate(signed)
log.log(signed, decision, reason, actor="admission-controller")
if decision.value == "deny":
raise SystemExit(f"blocked: {reason}")
# ... now hand `signed.program.bytecode` to bpf_prog_load().
Architecture
+-----------------+ +-----------+ +--------------------+
| Dev / CI | --> | bpftool, | --> | .bpf.o object |
| writes BPF C | | clang BPF | | (compiled) |
+-----------------+ +-----------+ +---------+----------+
|
v
+---------------------+
| BPFSigner (ML-DSA) |
| signs canonical |
| manifest (hash+meta)|
+----------+----------+
|
v
+---------------------+
| SignedBPFProgram |
| ships with .bpf.o |
+----------+----------+
|
deployment / OCI bundle / admission webhook |
v
+------------------+ +----------------+ +----------------+
| LoadPolicy |--->| BPFVerifier |-->| AttestationLog |
| (rules, allow- | | checks sig + | | (append-only) |
| list, size caps)| | hash match | +----------------+
+--------+---------+ +-------+--------+
| |
| allow | deny
v v
+-------------------+ +------------------+
| bpf_prog_load() | | rejected before |
| kernel accepts | | reaching kernel |
+-------------------+ +------------------+
Cryptography
| Primitive | Algorithm | Source |
|---|---|---|
| Digital signature | ML-DSA-65 (FIPS 204) | quantumshield.core.signatures |
| Bytecode hash | SHA3-256 | hashlib.sha3_256 |
| Canonical manifest | Sorted JSON, UTF-8 | Deterministic, compact |
| Identity | did:pqaid:<sha3-256(pk)> |
quantumshield.identity.agent |
The signature does not cover the raw bytecode - only metadata + SHA3-256(bytecode) + size. Bytecode integrity is checked by recomputing the hash at verification time. This keeps the signature envelope small (stable at a few hundred bytes of metadata + a ~3 KB ML-DSA-65 signature), regardless of the size of the eBPF object.
Policy Model
A LoadPolicy is an ordered list of PolicyRules. Each rule declares:
program_types- whichBPFProgramTypevalues the rule covers (e.g.,(KPROBE, TRACING)).allowed_signers- afrozenset[str]of DIDs permitted to sign programs for these types. Empty set means "any verified signer".require_signature- whether an invalid signature forces a deny (defaultTrue; turning this off is only for testing).max_bytecode_size- hard cap on bytecode size; default 2 MiB. Prevents signing-gate bypass via oversize or compressed programs.
Evaluation:
- Iterate rules in order. First rule whose
program_typesmatches is the chosen rule. - If no rule matches, return
default_decision(defaultDENY). - Apply the matching rule: size check, signature check, allow-list check.
- First failing check returns
DENYwith a human-readable reason.
policy.enforce(signed) raises UntrustedSignerError if the signer is not in the allow-list, or PolicyDeniedError for any other denial.
CLI Reference
# Sign a compiled BPF object.
pqc-bpf sign trace.bpf.o --name trace-read --type kprobe --author ops-team
# Verify an envelope. Exit 0 if valid, 1 otherwise.
pqc-bpf verify trace.bpf.o.sig.json
# Pretty-print metadata without verifying.
pqc-bpf info trace.bpf.o.sig.json
# Show version.
pqc-bpf --version
Integration Notes
This library is intentionally userspace-only and kernel-agnostic. It does not hook bpf() syscalls, does not ship an LSM module, and does not link against libbpf. Real enforcement requires wiring one of:
- A pre-load userspace verifier. A small daemon that replaces direct
bpf_prog_load()calls in your deployment: callers passSignedBPFProgramenvelopes, the daemon verifies and enforces, and only then calls into libbpf. Simplest model, easiest to audit. - An LSM hook. Kernels with
CONFIG_BPF_LSM=ycan attach a BPF LSM program tobpf_prog_loadthat rejects unsigned programs - with the signature verifier itself running in userspace over a ring buffer. Defense-in-depth but more involved. - An admission controller. For Kubernetes, gate CRDs that reference BPF programs (Cilium, Falco, Tetragon, bpfman) through a webhook that verifies and logs before the DaemonSet even schedules.
In every case, this library provides the envelope format, the verifier, the policy engine, and the audit log. The trust root is the set of signer DIDs you choose to put in your LoadPolicy.
Threat Model
| Threat | Mitigation |
|---|---|
Unsigned attacker-supplied .bpf.o loaded via CAP_BPF |
require_signature=True; no envelope -> no load |
| Legit-looking program signed by a rogue insider | Allow-list of DIDs; rogue DID not permitted |
| Bytecode swapped after signing (TOCTOU) | SHA3-256 hash in signed manifest; verifier recomputes |
| Signature replayed on a different program | Canonical manifest binds signature to exact metadata + hash |
| Oversize program smuggling raw weights back to attacker | max_bytecode_size cap |
| Future quantum adversary forges ECDSA-signed BPF object | ML-DSA-65 is NIST FIPS 204, resistant to Shor's algorithm |
| Tampering with audit record | In-memory append-only; sink to WORM store in production |
Out of scope: anything the kernel verifier itself misses (bounds-checker bypasses, stack overflow via helpers, JIT spraying). The library trusts the kernel's BPF verifier to do its job once a program is loaded.
Why PQC Matters for eBPF
eBPF programs are long-lived trust artifacts. A kernel probe shipped with an observability agent today can remain deployed for a decade across thousands of hosts. If the signature that authorizes it is RSA-2048 or secp256r1, a cryptographically relevant quantum computer appearing within that window lets any attacker forge new programs that pass the same gate - with no warning, because the forgery is indistinguishable from a legitimate signature. Rotating that gate to ML-DSA-65 from day one keeps the trust boundary intact on the ten-year horizon where eBPF-based infrastructure actually operates.
Examples
examples/sign_and_verify.py- sign, serialize, deserialize, verify a synthetic program.examples/enforce_load_policy.py- three signers (two trusted, one rogue) evaluated against a policy, audit log printed.examples/tampered_bytecode_rejected.py- mutate bytecode post-sign; hash-consistency check fires.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqc_ebpf_attestation-0.1.0.tar.gz.
File metadata
- Download URL: pqc_ebpf_attestation-0.1.0.tar.gz
- Upload date:
- Size: 19.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b7d40d35c1e03c9b67b53a741bf8ad7ba2bb55ce52b3549b31b2d92cfb8f9b8
|
|
| MD5 |
8e3a1bf7e39012765699977ae4532d07
|
|
| BLAKE2b-256 |
4f48508dd61b5b4370dbd13b8b558a444e7e00f410c33b5ec5ebd8b2a485aa6d
|
File details
Details for the file pqc_ebpf_attestation-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pqc_ebpf_attestation-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09ced4299a23b6308ac9c3983497d4f693a5a7bd39edbabe7c748e5216e3205a
|
|
| MD5 |
aebf8bb2c795a0b0c811e1b14b6b520f
|
|
| BLAKE2b-256 |
309d96e4efa20f1eaca5072d39272a60925f68f438cd9e48927eca00a442eb31
|