mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 02:26:43 +09:00
During short-code pairing the agent submitted its Ed25519 public key at enroll time, but the panel never checked that the poller actually held the matching private key -- identity rested entirely on the bearer enrollment_secret. Anyone who captured that secret (e.g. from a log or proxy) could claim the device's credentials. The agent now signs the enrollment_id with its pairing keypair and sends the base64 signature as X-Enrollment-Signature on each poll. pairing_service.poll verifies it against the enrolled hex pubkey (cryptography Ed25519) and rejects a present-but-invalid or wrong-key signature with InvalidEnrollmentError, before touching any state. So a stolen enrollment_secret alone no longer suffices -- the attacker also needs the private key. Verification is best-effort for backward compatibility: a poll with no signature still works (older agents), and becomes mandatory once signing agents have rolled out (tracked in SECURITY.md). Agent side: KeyPair.Sign(); pairing.Client gains an optional keypair via SetKeyPair(), wired in at both call sites (cmd/agent/pair.go and internal/pairdriver/driver.go); Poll attaches the signature header. Verified: backend 89 passed / 12 skipped (4 new PoP tests: valid sig accepted, invalid rejected, wrong-key rejected, no-sig backward-compatible); go build ./... + go vet + go test ./... all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
"""
|
|
Tests for Ed25519 proof-of-possession in the short-code pairing poll (bug D).
|
|
|
|
The agent enrolls a hex-encoded Ed25519 pubkey, then signs the enrollment_id
|
|
with the matching private key on each poll. The panel verifies that signature
|
|
when present, so a stolen enrollment_secret alone can't claim a device whose
|
|
private key the attacker lacks. Verification is best-effort for backward
|
|
compatibility: a poll with no signature still works (older agents), but a
|
|
present-but-invalid signature is rejected.
|
|
"""
|
|
import base64
|
|
|
|
import pytest
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
|
|
from app.services import pairing_service
|
|
from app.services.pairing_service import InvalidEnrollmentError
|
|
|
|
|
|
def _enroll():
|
|
"""Enroll a fresh keypair; return (private_key, enrollment dict)."""
|
|
priv = Ed25519PrivateKey.generate()
|
|
raw_pub = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
pubkey_hex = raw_pub.hex()
|
|
enr = pairing_service.enroll(pubkey_hex, "pass1234", "machine-pop", {})
|
|
return priv, enr
|
|
|
|
|
|
def _sig(priv, enrollment_id):
|
|
return base64.b64encode(priv.sign(enrollment_id.encode())).decode()
|
|
|
|
|
|
def test_poll_accepts_valid_signature(app):
|
|
priv, enr = _enroll()
|
|
eid, esec = enr['enrollment_id'], enr['enrollment_secret']
|
|
|
|
res = pairing_service.poll(eid, esec, signature=_sig(priv, eid))
|
|
# Not claimed yet, but the signature was accepted (no exception raised).
|
|
assert res['status'] == 'pending'
|
|
|
|
|
|
def test_poll_rejects_invalid_signature(app):
|
|
priv, enr = _enroll()
|
|
eid, esec = enr['enrollment_id'], enr['enrollment_secret']
|
|
bad_sig = base64.b64encode(b'\x00' * 64).decode()
|
|
|
|
with pytest.raises(InvalidEnrollmentError):
|
|
pairing_service.poll(eid, esec, signature=bad_sig)
|
|
|
|
|
|
def test_poll_rejects_signature_from_wrong_key(app):
|
|
_priv, enr = _enroll()
|
|
eid, esec = enr['enrollment_id'], enr['enrollment_secret']
|
|
attacker = Ed25519PrivateKey.generate()
|
|
wrong_sig = base64.b64encode(attacker.sign(eid.encode())).decode()
|
|
|
|
with pytest.raises(InvalidEnrollmentError):
|
|
pairing_service.poll(eid, esec, signature=wrong_sig)
|
|
|
|
|
|
def test_poll_without_signature_is_backward_compatible(app):
|
|
_priv, enr = _enroll()
|
|
eid, esec = enr['enrollment_id'], enr['enrollment_secret']
|
|
|
|
res = pairing_service.poll(eid, esec)
|
|
assert res['status'] == 'pending'
|