src/pqc_ebpf_attestation/cli.py
3.8 KB · 112 lines · python Raw
1 """Command-line interface for pqc-ebpf-attestation."""
2
3 from __future__ import annotations
4
5 import json
6 import sys
7
8 import click
9 from quantumshield.identity.agent import AgentIdentity
10
11 from pqc_ebpf_attestation import __version__
12 from pqc_ebpf_attestation.program import BPFProgram, BPFProgramMetadata, BPFProgramType
13 from pqc_ebpf_attestation.signer import BPFSigner, BPFVerifier, SignedBPFProgram
14
15
16 @click.group()
17 @click.version_option(version=__version__, prog_name="pqc-bpf")
18 def main() -> None:
19 """pqc-bpf - sign and verify eBPF programs with ML-DSA."""
20
21
22 @main.command()
23 @click.argument("bpf_file", type=click.Path(exists=True))
24 @click.option("--name", required=True, help="Program name")
25 @click.option(
26 "--type",
27 "program_type",
28 type=click.Choice([t.value for t in BPFProgramType]),
29 default=BPFProgramType.KPROBE.value,
30 )
31 @click.option("--author", default="")
32 @click.option("--identity-name", default="bpf-signer", help="Name of the signing identity")
33 @click.option(
34 "--output",
35 "-o",
36 type=click.Path(),
37 default=None,
38 help="Write signed envelope JSON to file (default: <bpf_file>.sig.json)",
39 )
40 def sign(
41 bpf_file: str,
42 name: str,
43 program_type: str,
44 author: str,
45 identity_name: str,
46 output: str | None,
47 ) -> None:
48 """Sign an eBPF program file."""
49 metadata = BPFProgramMetadata(
50 name=name,
51 program_type=BPFProgramType(program_type),
52 author=author,
53 )
54 program = BPFProgram.from_file(metadata, bpf_file)
55
56 identity = AgentIdentity.create(identity_name)
57 signer = BPFSigner(identity)
58 signed = signer.sign(program)
59
60 out_path = output or f"{bpf_file}.sig.json"
61 with open(out_path, "w", encoding="utf-8") as f:
62 json.dump(signed.to_dict(), f, indent=2)
63
64 click.echo(f"Signed {bpf_file} -> {out_path}")
65 click.echo(f" signer_did: {signed.signer_did}")
66 click.echo(f" algorithm: {signed.algorithm}")
67 click.echo(f" bytecode_hash: {signed.program.bytecode_hash}")
68
69
70 @main.command()
71 @click.argument("signed_json", type=click.Path(exists=True))
72 def verify(signed_json: str) -> None:
73 """Verify a signed eBPF envelope."""
74 with open(signed_json, encoding="utf-8") as f:
75 data = json.load(f)
76 signed = SignedBPFProgram.from_dict(data)
77 result = BPFVerifier.verify(signed)
78 if result.valid:
79 click.echo(f"[OK] {signed.program.metadata.name} - signature VALID")
80 click.echo(f" signer_did: {signed.signer_did}")
81 click.echo(f" program_type: {signed.program.metadata.program_type.value}")
82 click.echo(f" bytecode_hash: {signed.program.bytecode_hash}")
83 sys.exit(0)
84 else:
85 click.echo(f"[FAIL] {signed.program.metadata.name} - {result.error}", err=True)
86 sys.exit(1)
87
88
89 @main.command()
90 @click.argument("signed_json", type=click.Path(exists=True))
91 def info(signed_json: str) -> None:
92 """Show metadata about a signed envelope."""
93 with open(signed_json, encoding="utf-8") as f:
94 data = json.load(f)
95 signed = SignedBPFProgram.from_dict(data)
96 click.echo(f"program name: {signed.program.metadata.name}")
97 click.echo(f"program type: {signed.program.metadata.program_type.value}")
98 click.echo(f"author: {signed.program.metadata.author}")
99 click.echo(f"description: {signed.program.metadata.description}")
100 click.echo(f"kernel min: {signed.program.metadata.kernel_min}")
101 click.echo(f"attach point: {signed.program.metadata.attach_point}")
102 click.echo(f"license: {signed.program.metadata.license}")
103 click.echo(f"bytecode hash: {signed.program.bytecode_hash}")
104 click.echo(f"bytecode size: {signed.program.bytecode_size} bytes")
105 click.echo(f"signer_did: {signed.signer_did}")
106 click.echo(f"algorithm: {signed.algorithm}")
107 click.echo(f"signed_at: {signed.signed_at}")
108
109
110 if __name__ == "__main__":
111 main()
112