Skip to content

Commit b41ef3b

Browse files
Add SPAKE2-based automated pairing for client-worker trust
Agent-Logs-Url: https://github.com/codeSamuraii/pyfuse/sessions/4e3bd8dd-55b7-491e-936b-abf15a858f7d Co-authored-by: codeSamuraii <17270548+codeSamuraii@users.noreply.github.com>
1 parent 58c096b commit b41ef3b

4 files changed

Lines changed: 859 additions & 0 deletions

File tree

pyfuse/__main__.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,149 @@ def _add_keypair_parser(sub: "argparse._SubParsersAction[argparse.ArgumentParser
534534
fp.add_argument("key_file", help="Path to a .pem or .pub key file")
535535

536536

537+
def _cmd_pair(args: argparse.Namespace) -> None:
538+
"""Handle ``pyfuse pair accept|request`` subcommands."""
539+
action = getattr(args, "pair_action", None)
540+
if action is None:
541+
print("Usage: pyfuse pair {accept|request}", file=sys.stderr)
542+
sys.exit(1)
543+
544+
if action == "accept":
545+
asyncio.run(_pair_accept(args))
546+
elif action == "request":
547+
asyncio.run(_pair_request(args))
548+
549+
550+
async def _pair_accept(args: argparse.Namespace) -> None:
551+
"""Worker-side pairing: generate code, wait for client."""
552+
from pyfuse.core.pairing import (
553+
RedisPairingTransport,
554+
accept_pairing,
555+
generate_pairing_code,
556+
)
557+
558+
backend_url = args.backend or os.environ.get("PYFUSE_BACKEND")
559+
if not backend_url:
560+
print(
561+
"Error: --backend is required (or set PYFUSE_BACKEND).",
562+
file=sys.stderr,
563+
)
564+
sys.exit(1)
565+
566+
code = args.code or generate_pairing_code()
567+
timeout = args.timeout
568+
569+
transport = RedisPairingTransport(backend_url)
570+
print(f"Pairing code: {code}")
571+
print(f"Waiting for client... (expires in {timeout}s)")
572+
573+
try:
574+
result = await accept_pairing(
575+
transport,
576+
code,
577+
trusted_keys_dir=args.trusted_keys,
578+
timeout=timeout,
579+
)
580+
print(f"\u2713 Paired! Fingerprint: {result.fingerprint}")
581+
if args.trusted_keys:
582+
print(f" Key saved to {args.trusted_keys}/")
583+
except TimeoutError:
584+
print("\u2717 Pairing timed out — no client connected.", file=sys.stderr)
585+
sys.exit(1)
586+
except ValueError as exc:
587+
print(f"\u2717 Pairing failed: {exc}", file=sys.stderr)
588+
sys.exit(1)
589+
finally:
590+
await transport.close()
591+
592+
593+
async def _pair_request(args: argparse.Namespace) -> None:
594+
"""Client-side pairing: connect to worker with code."""
595+
from pyfuse.core.pairing import RedisPairingTransport, request_pairing
596+
597+
backend_url = args.backend or os.environ.get("PYFUSE_BACKEND")
598+
if not backend_url:
599+
print(
600+
"Error: --backend is required (or set PYFUSE_BACKEND).",
601+
file=sys.stderr,
602+
)
603+
sys.exit(1)
604+
605+
if not args.code:
606+
print("Error: --code is required.", file=sys.stderr)
607+
sys.exit(1)
608+
609+
transport = RedisPairingTransport(backend_url)
610+
611+
try:
612+
result = await request_pairing(
613+
transport,
614+
args.code,
615+
save_path=args.output,
616+
timeout=args.timeout,
617+
)
618+
print(f"\u2713 Paired with worker! Fingerprint: {result.fingerprint}")
619+
if args.output:
620+
print(f" Private key: {args.output}")
621+
print(f" Public key: {Path(args.output).with_suffix('.pub')}")
622+
except TimeoutError:
623+
print("\u2717 Pairing timed out — no worker responded.", file=sys.stderr)
624+
sys.exit(1)
625+
except ValueError as exc:
626+
print(f"\u2717 Pairing failed: {exc}", file=sys.stderr)
627+
sys.exit(1)
628+
finally:
629+
await transport.close()
630+
631+
632+
def _add_pair_parser(sub: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
633+
p = sub.add_parser(
634+
"pair",
635+
help="Automated SPAKE2-based client-worker pairing",
636+
)
637+
pair_sub = p.add_subparsers(dest="pair_action")
638+
639+
accept = pair_sub.add_parser(
640+
"accept", help="Worker side: generate a pairing code and wait for a client",
641+
)
642+
accept.add_argument(
643+
"--backend", default=None,
644+
help="Backend URL (default: $PYFUSE_BACKEND)",
645+
)
646+
accept.add_argument(
647+
"--code", default=None,
648+
help="Use a specific pairing code instead of generating one",
649+
)
650+
accept.add_argument(
651+
"--trusted-keys", default=None, metavar="DIR",
652+
help="Directory to save the client's .pub key file",
653+
)
654+
accept.add_argument(
655+
"--timeout", type=float, default=60.0,
656+
help="Seconds to wait for a client (default: 60)",
657+
)
658+
659+
request = pair_sub.add_parser(
660+
"request", help="Client side: pair with a worker using a pairing code",
661+
)
662+
request.add_argument(
663+
"--backend", default=None,
664+
help="Backend URL (default: $PYFUSE_BACKEND)",
665+
)
666+
request.add_argument(
667+
"--code", default=None,
668+
help="Pairing code displayed by the worker",
669+
)
670+
request.add_argument(
671+
"-o", "--output", default=None,
672+
help="Path to save the generated keypair (default: pyfuse_key.pem)",
673+
)
674+
request.add_argument(
675+
"--timeout", type=float, default=60.0,
676+
help="Seconds to wait for the worker (default: 60)",
677+
)
678+
679+
537680
def _build_parser() -> argparse.ArgumentParser:
538681
parser = argparse.ArgumentParser(
539682
prog="pyfuse", description="pyfuse - distributed task execution",
@@ -546,6 +689,7 @@ def _build_parser() -> argparse.ArgumentParser:
546689
_add_reconstruct_parser(sub)
547690
_add_sandbox_parser(sub)
548691
_add_keypair_parser(sub)
692+
_add_pair_parser(sub)
549693
return parser
550694

551695

@@ -557,6 +701,7 @@ def _build_parser() -> argparse.ArgumentParser:
557701
"reconstruct": _cmd_reconstruct,
558702
"sandbox": _cmd_sandbox,
559703
"keypair": _cmd_keypair,
704+
"pair": _cmd_pair,
560705
}
561706

562707

0 commit comments

Comments
 (0)