Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/verify_ucp_flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DBAdapter {
async createWallet(data) { return Wallet.create(data); }
async createTransaction(data) { return Transaction.create(data); }
async updateTransaction(id, data) { return Transaction.findByIdAndUpdate(id, data, { new: true }); }
async findAgentById(id) { return Agent.findById(id); }

async updateWalletBalance(walletId, amount) {
return Wallet.findByIdAndUpdate(walletId, { $inc: { balance: amount } }, { new: true });
Expand Down
34 changes: 29 additions & 5 deletions src/services/a2aService.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
* policy compliance, limit checks, and authorized counterparty validation.
*/

const { Agent } = require("../models/agent");
const MandateService = require("./mandate");
const logger = require("../utils/logger");

class A2AService {
constructor(walletService, db) {
constructor(walletService, db, config = {}) {
this.walletService = walletService;
this.db = db;
this.mandateService = new MandateService(config.mandateConfig);
this.strictMandateMode =
config.strictMandateMode !== undefined
? config.strictMandateMode
: process.env.STRICT_MANDATE_MODE === "true";
}

/**
Expand All @@ -20,17 +25,36 @@ class A2AService {
* @param {string} params.fromAgentId - Sender Agent ID
* @param {string} params.toAgentId - Recipient Agent ID
* @param {number} params.amount - Amount to transfer
* @param {string} params.mandate - Optional signed Mandate (AP2) for Zero Trust validation
* @param {Object} params.ucpPayload - The original UCP intent/payload
*/
async executeTransfer({ fromAgentId, toAgentId, amount, ucpPayload = {} }) {
async executeTransfer({
fromAgentId,
toAgentId,
amount,
mandate,
ucpPayload = {},
}) {
try {
// 0. Zero Trust Mandate Validation
if (mandate) {
await this.mandateService.verifyMandate(mandate, {
amount,
recipient: toAgentId,
});
} else if (this.strictMandateMode) {
throw new Error(
"Zero Trust Validation Failed: Mandate required for A2A transfer in strict mode",
);
}

// 1. Validate Agents
const fromAgent = await Agent.findById(fromAgentId);
const fromAgent = await this.db.findAgentById(fromAgentId);
if (!fromAgent || fromAgent.status !== "active") {
throw new Error(`Sender agent ${fromAgentId} not found or inactive`);
}

const toAgent = await Agent.findById(toAgentId);
const toAgent = await this.db.findAgentById(toAgentId);
if (!toAgent || toAgent.status !== "active") {
throw new Error(`Recipient agent ${toAgentId} not found or inactive`);
}
Expand Down
60 changes: 57 additions & 3 deletions src/services/mandate.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,68 @@ class MandateService {
/**
* Verify a Mandate (Intent or Cart)
* @param {string} token - Signed JWT Mandate
* @param {Object} context - Optional transaction context for validation (amount, recipient)
* @returns {Promise<Object>} Decoded mandate payload
*/
async verifyMandate(token) {
async verifyMandate(token, context = {}) {
let decoded;
try {
return jwt.verify(token, this.signingKey, { algorithms: ["HS256"] });
decoded = jwt.verify(token, this.signingKey, { algorithms: ["HS256"] });
} catch (error) {
throw new Error(`Zero Trust Validation Failed: Mandate verification failed: ${error.message}`);
if (error.message?.includes("jwt expired")) {
throw new Error("Zero Trust Validation Failed: Mandate has expired");
}
throw new Error(
`Zero Trust Validation Failed: Mandate verification failed: ${error.message}`,
);
}

// 1. Validate expiration (redundant with JWT but good for explicit error)
if (decoded.exp < Math.floor(Date.now() / 1000)) {
throw new Error("Zero Trust Validation Failed: Mandate has expired");
}

// 2. Validate budget if context amount is provided
if (context.amount) {
// Check Intent Mandate budget
if (
decoded.max_budget &&
context.amount > decoded.max_budget.value
) {
throw new Error(
`Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decoded.max_budget.value}`,
);
}
// Check Cart Mandate total price
if (
decoded.total_price &&
context.amount !== decoded.total_price
) {
throw new Error(
`Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decoded.total_price}`,
);
}
}

// 3. Validate recipient/merchant if context recipient is provided
if (context.recipient) {
// Check Intent Mandate allowed merchants
if (decoded.allowed_merchants?.length > 0) {
if (!decoded.allowed_merchants.includes(context.recipient)) {
throw new Error(
`Zero Trust Validation Failed: Merchant ${context.recipient} not authorized by mandate`,
);
}
}
// Check Cart Mandate merchant_did
if (decoded.merchant_did && decoded.merchant_did !== context.recipient) {
throw new Error(
`Zero Trust Validation Failed: Recipient ${context.recipient} does not match cart mandate merchant ${decoded.merchant_did}`,
);
}
}

return decoded;
}

/**
Expand Down
51 changes: 9 additions & 42 deletions src/services/tokenization.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,53 +331,20 @@ class TokenizationService {
async signWithToken(tokenId, dataToSign, mandate, context = {}) {
// Zero Trust Validation: Verify mandate BEFORE entering try/catch simulation block
if (mandate) {
let decodedMandate;
try {
decodedMandate = await this.mandateService.verifyMandate(mandate);
// Normalize context for MandateService (merchant -> recipient)
const validationContext = {
...context,
recipient: context.recipient || context.merchant,
};
await this.mandateService.verifyMandate(mandate, validationContext);
} catch (error) {
if (error.message?.includes("jwt expired")) {
throw new Error("Zero Trust Validation Failed: Mandate has expired");
}
if (error.message?.includes("Zero Trust Validation Failed:")) {
throw error;
}
throw new Error(`Zero Trust Validation Failed: ${error.message || error}`);
}

// Validate budget if context amount is provided
if (context.amount) {
// Check Intent Mandate budget
if (
decodedMandate.max_budget &&
context.amount > decodedMandate.max_budget.value
) {
throw new Error(
`Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decodedMandate.max_budget.value}`,
);
}
// Check Cart Mandate total price
if (
decodedMandate.total_price &&
context.amount !== decodedMandate.total_price
) {
throw new Error(
`Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decodedMandate.total_price}`,
);
}
}

// Validate merchant if context merchant is provided
if (context.merchant && decodedMandate.allowed_merchants?.length > 0) {
if (!decodedMandate.allowed_merchants.includes(context.merchant)) {
throw new Error(
`Zero Trust Validation Failed: Merchant ${context.merchant} not authorized by mandate`,
);
}
}

// Validate expiration
if (decodedMandate.exp < Math.floor(Date.now() / 1000)) {
throw new Error("Zero Trust Validation Failed: Mandate has expired");
throw new Error(
`Zero Trust Validation Failed: ${error.message || error}`,
);
}
} else if (this.strictMandateMode) {
throw new Error(
Expand Down
87 changes: 87 additions & 0 deletions tests/unit/a2a_zerotrust.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const MandateService = require('../../src/services/mandate');
const A2AService = require('../../src/services/a2aService');

describe('A2AService Zero Trust Validation', () => {
let a2aService;
let mandateService;
let mockWalletService;
let mockDb;

beforeEach(() => {
mockWalletService = {
transfer: jest.fn().mockResolvedValue({ transferId: 'tx_123' })
};
mockDb = {
findAgentById: jest.fn().mockImplementation((id) => ({
id,
name: `Agent ${id}`,
status: 'active',
walletId: `wallet_${id}`,
config: { limits: { perTransaction: 1000 } }
}))
};
mandateService = new MandateService({ signingKey: 'test-secret' });
a2aService = new A2AService(mockWalletService, mockDb, {
mandateConfig: { signingKey: 'test-secret' },
strictMandateMode: true
});
});

it('should fail if mandate is missing in strict mode', async () => {
await expect(a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent2',
amount: 100
})).rejects.toThrow('Zero Trust Validation Failed: Mandate required for A2A transfer in strict mode');
});

it('should fail if mandate budget is exceeded', async () => {
const mandate = await mandateService.issueIntentMandate({
userDid: 'did:user:1',
agentDid: 'did:agent:1',
maxBudget: 50
});

await expect(a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent2',
amount: 100,
mandate
})).rejects.toThrow('Zero Trust Validation Failed: Amount 100 exceeds mandate budget of 50');
});

it('should fail if recipient is not authorized by mandate', async () => {
const mandate = await mandateService.issueIntentMandate({
userDid: 'did:user:1',
agentDid: 'did:agent:1',
maxBudget: 200,
allowedMerchants: ['agent2']
});

await expect(a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent3',
amount: 100,
mandate
})).rejects.toThrow('Zero Trust Validation Failed: Merchant agent3 not authorized by mandate');
});

it('should succeed with valid mandate', async () => {
const mandate = await mandateService.issueIntentMandate({
userDid: 'did:user:1',
agentDid: 'did:agent:1',
maxBudget: 200,
allowedMerchants: ['agent2']
});

const result = await a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent2',
amount: 100,
mandate
});

expect(result.success).toBe(true);
expect(mockWalletService.transfer).toHaveBeenCalled();
});
});
Loading