Skip to content

Commit 92fd47e

Browse files
committed
fix(security): add cryptography dependency and fix test location for CI
1 parent 5bf0792 commit 92fd47e

File tree

4 files changed

+380
-179
lines changed

4 files changed

+380
-179
lines changed

packages/agent-os/modules/nexus/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies = [
2626
"structlog>=24.1.0",
2727
"aiohttp>=3.13.3",
2828
"inter-agent-trust-protocol>=0.4.0",
29+
"cryptography>=41.0.0",
2930
]
3031

3132
[project.urls]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
import pytest
5+
from pydantic import BaseModel
6+
from datetime import datetime, timezone
7+
from .. import crypto
8+
9+
class MockModel(BaseModel):
10+
name: str
11+
value: int
12+
timestamp: datetime
13+
signature: str = ""
14+
15+
def test_canonical_payload_stability():
16+
"""Ensure dict and pydantic models produce same canonical output."""
17+
ts = datetime(2024, 1, 1, tzinfo=timezone.utc)
18+
data_dict = {"name": "test", "value": 42, "timestamp": ts, "signature": "ignore-me"}
19+
data_model = MockModel(name="test", value=42, timestamp=ts, signature="ignore-me")
20+
21+
payload1 = crypto.canonical_payload(data_dict)
22+
payload2 = crypto.canonical_payload(data_model)
23+
24+
assert payload1 == payload2
25+
# Check that signature field is excluded
26+
assert b"ignore-me" not in payload1
27+
# Check that keys are sorted (name before value)
28+
assert payload1.find(b"name") < payload1.find(b"value")
29+
30+
def test_sign_and_verify_roundtrip():
31+
"""Test full sign/verify cycle."""
32+
priv, pub_str = crypto.generate_keypair()
33+
data = {"agent_did": "did:nexus:test", "action": "register"}
34+
35+
sig_hex = crypto.sign_data(priv, data)
36+
assert len(sig_hex) == 128 # 64 bytes hex encoded
37+
38+
# Should not raise
39+
crypto.verify_signature(pub_str, sig_hex, data)
40+
41+
def test_verify_fails_on_tamper():
42+
"""Test that tampered data fails verification."""
43+
priv, pub_str = crypto.generate_keypair()
44+
data = {"agent_did": "did:nexus:test", "action": "register"}
45+
sig_hex = crypto.sign_data(priv, data)
46+
47+
tampered_data = {"agent_did": "did:nexus:EVIL", "action": "register"}
48+
49+
with pytest.raises(crypto.SignatureVerificationError):
50+
crypto.verify_signature(pub_str, sig_hex, tampered_data)
51+
52+
def test_verify_fails_on_wrong_key():
53+
"""Test that verification fails with a different key."""
54+
priv1, pub_str1 = crypto.generate_keypair()
55+
priv2, pub_str2 = crypto.generate_keypair()
56+
57+
data = {"agent_did": "did:nexus:test"}
58+
sig_hex = crypto.sign_data(priv1, data)
59+
60+
with pytest.raises(crypto.SignatureVerificationError):
61+
crypto.verify_signature(pub_str2, sig_hex, data)
62+
63+
def test_decode_error():
64+
"""Test behavior with malformed hex."""
65+
_, pub_str = crypto.generate_keypair()
66+
with pytest.raises(crypto.SignatureDecodeError):
67+
crypto.verify_signature(pub_str, "not-a-hex-string", {"data": 1})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
import pytest
5+
from nexus.escrow import ProofOfOutcome, EscrowManager
6+
from nexus.reputation import ReputationEngine
7+
from nexus import crypto
8+
9+
@pytest.fixture
10+
def reputation_engine():
11+
return ReputationEngine(trust_threshold=500)
12+
13+
@pytest.fixture
14+
def escrow_manager(reputation_engine):
15+
em = EscrowManager(reputation_engine=reputation_engine)
16+
em.add_credits("did:nexus:requester-agent", 1000)
17+
return em
18+
19+
@pytest.fixture
20+
def proof_of_outcome(escrow_manager):
21+
return ProofOfOutcome(escrow_manager=escrow_manager)
22+
23+
@pytest.mark.asyncio
24+
async def test_create_escrow_with_signing(proof_of_outcome, escrow_manager):
25+
priv, pub = crypto.generate_keypair()
26+
27+
# Create escrow with signing
28+
receipt = await proof_of_outcome.create_escrow(
29+
requester_did="did:nexus:requester-agent",
30+
provider_did="did:nexus:provider-agent",
31+
task_hash="abc123def456",
32+
credits=100,
33+
private_key=priv
34+
)
35+
36+
assert receipt.requester_signature is not None
37+
# Verify the signature matches
38+
crypto.verify_signature(pub, receipt.requester_signature, receipt.request)
39+
40+
@pytest.mark.asyncio
41+
async def test_create_escrow_legacy_signature(proof_of_outcome):
42+
# Create escrow WITHOUT signing (legacy mode)
43+
receipt = await proof_of_outcome.create_escrow(
44+
requester_did="did:nexus:requester-agent",
45+
provider_did="did:nexus:provider-agent",
46+
task_hash="abc123def456",
47+
credits=100
48+
)
49+
50+
# Legacy signature should be in the expected format: sig_{requester_did}_{task_hash[:8]}
51+
expected_sig = "sig_did:nexus:requester-agent_abc123de"
52+
assert receipt.requester_signature == expected_sig

0 commit comments

Comments
 (0)