Version: 1.0
Date: 2026-03-21
Applies to: acp_relay.py v1.0.0+
ACP provides two optional, independent security mechanisms:
| Mechanism | Purpose | Requires |
|---|---|---|
| HMAC-SHA256 (§1) | Message integrity + peer authentication for closed deployments | Shared secret (out-of-band) |
| Ed25519 Identity (§2) | Cryptographic sender identity for open deployments | pip install cryptography |
Both can be active simultaneously — they are complementary, not mutually exclusive.
What ACP does NOT provide:
- Confidentiality (messages are plaintext over WebSocket/HTTP)
- Forward secrecy
- Key exchange / key agreement protocol
- Certificate authority or PKI
For confidentiality, terminate TLS at a load balancer or reverse proxy (see §4).
When --secret <key> is configured, every outbound message is signed:
sig = HMAC-SHA256(secret_bytes, "{message_id}:{ts}").hexdigest()
The resulting hex string is appended as the sig field to the message envelope.
Inbound verification: if sig is present in a received message and a secret is configured,
the relay verifies using hmac.compare_digest() (constant-time, no timing oracle). On mismatch:
- A warning is logged
- The message is not dropped (warn-only, per ACP spec §7.1)
- The message object has
_sig_invalid=trueset for application-level inspection
If no secret is configured, sig fields in received messages are silently ignored
(backward-compatible interoperability).
| Check | Result | Notes |
|---|---|---|
| Constant-time comparison | ✅ PASS | hmac.compare_digest() used throughout |
| Timing oracle in error response | ✅ PASS | Error path identical for valid/invalid sig |
| Payload determinism | ✅ PASS | f"{message_id}:{ts}" — fixed format |
| message_id unpredictability | ✅ PASS | msg_<16 hex chars> from secrets.token_hex(8) |
| Secret stored in memory only | ✅ PASS | _hmac_secret: bytes, never written to disk |
| Replay attack prevention | ✅ PASS | --hmac-window (v1.1): inbound ts must be within ±300 s of server clock; out-of-window messages are dropped (hard reject). Default window: 300 s, configurable via --hmac-window <seconds>. |
| Key length recommendation | ℹ️ INFO | Any length accepted; recommend ≥32 bytes for 128-bit security |
ACP HMAC enforces a server-side timestamp window check when --secret is set.
- Mechanism: inbound
tsfield (ISO-8601 UTC) is compared to server clock. If|now - ts| > HMAC_REPLAY_WINDOW, the message is dropped (hard reject, not warn). - Default window: 300 seconds (5 minutes) — configurable via
--hmac-window <seconds>. - Config file: key
hmac-windowin JSON/YAML config is also supported. - Graceful degradation: when
--secretis not set, the window check is skipped (no-op).
Example:
acp-relay --secret mysecret --hmac-window 120 # 2-minute window
Risk level: Window value should balance clock-skew tolerance vs. replay risk. For high-security deployments, use 60–120 s; for normal agent-to-agent, 300 s is sufficient.
| Threat | Protected? |
|---|---|
| Message tampering in transit | ✅ Yes (integrity) |
| Impersonation by unknown peer | ✅ Yes (authentication) |
| Eavesdropping / confidentiality | ❌ No (use TLS, §4) |
| Replay attacks | |
| Key compromise | ❌ No — rotate secret out-of-band |
When --identity [path] is configured, every outbound message includes an identity block:
"identity": {
"scheme": "ed25519",
"public_key": "<base64url 32-byte public key>",
"sig": "<base64url 64-byte Ed25519 signature>"
}Signing input: canonical JSON of the full message envelope, excluding identity.sig:
canonical = {k: v for k, v in msg.items() if k != "identity"}
payload = json.dumps(canonical, sort_keys=True, ensure_ascii=False, separators=(",",":")).encode()The canonical form uses sort_keys=True and compact separators to ensure byte-for-byte
reproducibility across implementations.
Keypair storage: ~/.acp/identity.json (auto-generated on first run, chmod 0600).
Verification: warn-only on mismatch; message is accepted even if cryptography not installed.
| Check | Result | Notes |
|---|---|---|
| Key file permissions | ✅ PASS | path.chmod(0o600) enforced on creation |
| Canonical form determinism | ✅ PASS | sort_keys=True, separators=(",",":") — byte-identical across Python versions |
identity.sig excluded from payload |
✅ PASS | {k: v for k, v in msg.items() if k != "identity"} |
InvalidSignature exception handling |
✅ PASS | Caught, returns False; no exception leak |
Graceful fallback without cryptography |
✅ PASS | _ED25519_AVAILABLE flag checked at call sites |
| Key generation entropy | ✅ PASS | Ed25519PrivateKey.generate() uses OS CSPRNG |
| Private key in-memory only | ✅ PASS | _ed25519_private never serialized post-load |
| Key rotation | ℹ️ INFO | Delete ~/.acp/identity.json and restart to rotate |
| Threat | Protected? |
|---|---|
| Sender impersonation | ✅ Yes (cryptographic proof of origin) |
| Message tampering | ✅ Yes (signature covers full envelope) |
| Non-repudiation | ✅ Yes (verifiable by any party with public key) |
| Confidentiality | ❌ No (public key visible; use TLS, §4) |
| Forward secrecy | ❌ No (static keypair) |
| Key revocation | ❌ No (no PKI/CRL; manual rotation only) |
HMAC and Ed25519 may be active simultaneously:
acp-relay --secret "shared-key" --identity- HMAC
sigis computed first, then included in the Ed25519 signing payload - This means Ed25519 signature covers the HMAC sig — no ordering vulnerability
- AgentCard reports both:
capabilities.hmac_signing=trueandcapabilities.identity="ed25519"
| Property | HMAC-SHA256 | Ed25519 Identity |
|---|---|---|
| Key type | Symmetric (shared secret) | Asymmetric (keypair) |
| Requires shared state | Yes (both sides need secret) | No (public key in AgentCard) |
| Verifiable by third party | No | Yes |
| Non-repudiation | No | Yes |
| Best for | Closed deployments (known peers) | Open deployments (any peer) |
| Setup | --secret <key> |
--identity (auto-generates) |
| Dependency | stdlib hmac |
cryptography (optional) |
ACP over plain HTTP/WS provides no confidentiality. For production deployments:
[Agent A] ──WebSocket──► [nginx/caddy TLS termination] ──► [Agent B :7801]
[Client] ──HTTPS──────► [nginx/caddy TLS termination] ──► [relay :7901]
Example (Caddy):
youragent.example.com {
reverse_proxy localhost:7901
reverse_proxy /ws localhost:7801
}
cloudflared tunnel --url http://localhost:7901
# Provides HTTPS + certificate automaticallyacp-relay --relay "acp+wss://your-worker.workers.dev/..."This transport uses HTTPS by default. See transports.md §Binding-C.
| Limitation | Severity | Workaround / Roadmap |
|---|---|---|
| Clock skew vs replay-window tradeoff | Info | Default 300 s window; tune with --hmac-window |
| No confidentiality | Medium | Use TLS termination (§4) |
| No forward secrecy | Low–Medium | Rotate secret/keypair periodically |
| No key revocation infrastructure | Low | Manual rotation: delete ~/.acp/identity.json |
| Ed25519 verification is warn-only | By design | Per ACP spec §7.2: graceful degradation |
| Version | Date | Auditor | Findings |
|---|---|---|---|
| v1.0.0 | 2026-03-21 | J.A.R.V.I.S. (internal) | 8 PASS, 1 PARTIAL (replay window), 2 INFO — see §1.2 and §2.2 |
| v1.1.0 | 2026-03-22 | J.A.R.V.I.S. (internal) | 9 PASS, 0 PARTIAL — replay-window --hmac-window implemented (§1.3) |
Security questions or disclosures: open an issue at https://github.com/Kickflip73/agent-communication-protocol/issues