Skip to content

[Bug]: Security Inconsistencies Between Specification and Implementation in Mandate Design #150

@AdijeShen

Description

@AdijeShen

What happened?

Issue: Security Inconsistencies Between Specification and Implementation in Mandate Design

Issue Type

Bug / Security Concern

Severity

High - Security Implications

Summary

The AP2 codebase has several critical inconsistencies between the specification documents and the actual code implementation regarding mandate signing and security mechanisms. These inconsistencies create potential security vulnerabilities and should be addressed before production deployment.

Description

The AP2 repository defines three types of mandates (Intent, Cart, and Payment) and provides comprehensive specification documents, but the actual implementation lacks several critical security features described in the specs. This creates a gap where malicious actors could potentially:

  1. Tamper with unsigned Intent Mandates
  2. Execute unauthorized transactions in Human Not Present scenarios
  3. Bypass user authorization checks

Specific Inconsistencies

1. Intent Mandate Missing User Signature Field

Specification States (docs/specification.md:306-312):

"The Intent Mandate is a separate verifiable digital credential which is critical for scenarios where the human is not present at actual transaction time. It serves as the final, non-repudiable authorization to execute a purchase in the user's absence. It is generated by the Shopping Agent based on the user's request and is cryptographically signed by the user, typically using a hardware-backed key on their device."

Code Implementation (src/ap2/types/mandate.py:32-76):

class IntentMandate(BaseModel):
  """Represents the user's purchase intent."""

  user_cart_confirmation_required: bool = Field(...)
  natural_language_description: str = Field(...)
  merchants: Optional[list[str]] = Field(None)
  skus: Optional[list[str]] = Field(None)
  requires_refundability: Optional[bool] = Field(False)
  intent_expiry: str = Field(...)

  # ❌ MISSING: No user_authorization or signature field

Impact: Without a user signature field, a malicious shopping agent could tamper with the Intent Mandate after user confirmation but before merchant validation. The security constraint in the code (user_cart_confirmation_required must be true if not signed) exists in comments but there's no field to actually store the signature.


2. Payment Mandate Lacks Intent Mandate Reference in HNP Scenarios

Specification States (docs/specification.md:330-335):

"While the Cart and Intent mandates are required by the merchant to fulfill the order, separately the protocol provides additional visibility into the agentic transaction to the payments ecosystem. For this purpose, a verifiable digital credential 'PaymentMandate' (bound to Cart/Intent mandate but containing separate information) may be shared with the network/issuer..."

The specification clearly indicates that Payment Mandate should be "bound to Cart/Intent mandate".

Code Implementation (src/ap2/types/mandate.py:137-162):

class PaymentMandateContents(BaseModel):
  """The data contents of a PaymentMandate."""

  payment_mandate_id: str = Field(...)
  payment_details_id: str = Field(...)
  payment_details_total: PaymentItem = Field(...)
  payment_response: PaymentResponse = Field(...)
  merchant_agent: str = Field(...)
  timestamp: str = Field(...)

  # ❌ MISSING: No intent_mandate_id or intent_mandate_hash field
  # ❌ MISSING: No transaction_modality field to indicate HP vs HNP

Impact: In Human Not Present scenarios, the payment network cannot verify that the "human not present" transaction has proper pre-authorization from a user-signed Intent Mandate. This breaks the audit trail and dispute resolution mechanism.

Note on Cart Mandate Binding:
The current implementation correctly includes cart_mandate_hash in the user's signature (samples/python/src/roles/shopping_agent/tools.py:250-252):

payment_mandate.user_authorization = (
    cart_mandate_hash + "_" + payment_mandate_hash
)

The cart_mandate_hash is computed from the complete Cart Mandate object, which includes the merchant's signature (merchant_authorization field). This means the user's signature indirectly binds to the merchant-signed Cart Mandate through the hash.

However, the same binding mechanism is missing for Intent Mandate in Human Not Present scenarios.


3. Clarification Needed on Cart Mandate Signing Mechanism

Specification Describes Dual Signature Model:

Location A (docs/specification.md:285-288):

"It is generated by the Merchant based on the user's request and is cryptographically signed by the user, typically using a hardware-backed key on their device with in-session authentication."

Location B (docs/specification.md:675-678):

"The cart mandate is first signed by the merchant entity (not an Agent) to guarantee they will fulfill the order based on the SKU, price and shipping information."

Code Implementation (src/ap2/types/mandate.py:107-134):

class CartMandate(BaseModel):
  """A cart whose contents have been digitally signed by the merchant."""

  contents: CartContents = Field(...)
  merchant_authorization: Optional[str] = Field(
      None,
      description="A base64url-encoded JSON Web Token (JWT) that digitally signs the cart contents..."
  )
  # ❌ No separate user_signature field on Cart Mandate

Correct Understanding: The Cart Mandate uses a dual signature model:

  1. Merchant Signature: merchant_authorization field - merchant signs the cart to guarantee fulfillment
  2. User Reference: User signs the Payment Mandate, which includes cart_mandate_hash - this serves as the user's approval of the merchant-signed cart

The user's "signature" on the Cart Mandate is not a direct signature on the Cart Mandate object itself, but rather an indirect signature through the Payment Mandate's binding to cart_mandate_hash.

Impact: The specification wording could be clearer. Section 286 should clarify that the user's "signature" on the Cart Mandate is achieved through the Payment Mandate's binding to the Cart Mandate hash, not a direct signature on the Cart Mandate object.


Proposed Solutions

Solution 1: Add User Authorization Field to Intent Mandate

class IntentMandate(BaseModel):
  """Represents the user's purchase intent."""

  user_cart_confirmation_required: bool = Field(...)
  natural_language_description: str = Field(...)
  merchants: Optional[list[str]] = Field(None)
  skus: Optional[list[str]] = Field(None)
  requires_refundability: Optional[bool] = Field(False)
  intent_expiry: str = Field(...)

  # ✅ ADD THIS:
  user_authorization: Optional[str] = Field(
      None,
      description=(
          """
          A base64url-encoded JSON Web Token (JWT) that digitally signs the
          intent mandate contents by the user's private key. This provides
          non-repudiable proof of the user's intent and prevents tampering
          by the shopping agent.

          If this field is present, user_cart_confirmation_required can be
          set to false, allowing the agent to execute purchases in the
          user's absence.

          If this field is None, user_cart_confirmation_required must be true,
          requiring the user to confirm each specific purchase.
          """
      ),
      example="eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXhhbXBsZ...",
  )

Solution 2: Add Intent Mandate Reference to Payment Mandate

class PaymentMandateContents(BaseModel):
  """The data contents of a PaymentMandate."""

  payment_mandate_id: str = Field(...)
  payment_details_id: str = Field(...)
  payment_details_total: PaymentItem = Field(...)
  payment_response: PaymentResponse = Field(...)
  merchant_agent: str = Field(...)
  timestamp: str = Field(...)

  # ✅ ADD THIS:
  intent_mandate_id: Optional[str] = Field(
      None,
      description=(
          "Reference to the user-signed Intent Mandate that authorizes "
          "this transaction in Human Not Present scenarios. This allows "
          "the payment network to verify that the 'human not present' "
          "transaction has pre-authorization support from a 'human present' "
          "intent mandate. Required for HNP transactions."
      )
  )

  # ✅ ADD THIS:
  transaction_modality: Optional[str] = Field(
      None,
      description=(
          "Transaction modality: 'human_present' or 'human_not_present'. "
          "This signals to the payment network whether the user was present "
          "at the time of payment authorization."
      ),
      enum=["human_present", "human_not_present"]
  )

Solution 3: Update Payment Mandate Signature to Include Intent Mandate Hash (HNP Only)

def sign_mandates_on_user_device(tool_context: ToolContext) -> str:
  payment_mandate: PaymentMandate = tool_context.state["payment_mandate"]
  cart_mandate: CartMandate = tool_context.state["cart_mandate"]
  intent_mandate: Optional[IntentMandate] = tool_context.state.get("intent_mandate")

  cart_mandate_hash = _generate_cart_mandate_hash(cart_mandate)
  payment_mandate_hash = _generate_payment_mandate_hash(
      payment_mandate.payment_mandate_contents
  )

  # Current implementation includes cart_mandate_hash and payment_mandate_hash
  # The cart_mandate_hash binds to the merchant-signed Cart Mandate
  hashes = [cart_mandate_hash, payment_mandate_hash]

  # ✅ ADD THIS for Human Not Present scenarios:
  if intent_mandate:
      intent_mandate_hash = _generate_intent_mandate_hash(intent_mandate)
      hashes.append(intent_mandate_hash)

  payment_mandate.user_authorization = "_".join(hashes)

  tool_context.state["signed_payment_mandate"] = payment_mandate
  return payment_mandate.user_authorization

Note: The existing cart_mandate_hash binding is correct. The hash is computed from the complete Cart Mandate object, which includes the merchant's merchant_authorization field. This creates a chain: Merchant signs Cart Mandate → Cart Mandate Hash → User signs Payment Mandate (including Cart Mandate Hash). The missing piece is the Intent Mandate binding for HNP scenarios.

Solution 4: Clarify Specification Documentation on Cart Mandate Signing

Improve clarity in docs/specification.md:285-288:

Current wording (potentially confusing):

"It is generated by the Merchant based on the user's request and is cryptographically signed by the user, typically using a hardware-backed key on their device with in-session authentication."

Suggested clarification:

"It is generated by the Merchant based on the user's request. The merchant first signs the Cart Mandate to guarantee fulfillment. The user then approves the merchant-signed Cart Mandate by including its hash in their Payment Mandate signature, typically using a hardware-backed key on their device with in-session authentication."

This clarifies the dual signature model: merchant signs the cart directly, user approves indirectly through Payment Mandate binding.


Affected Components

  • src/ap2/types/mandate.py - Type definitions
  • samples/python/src/roles/shopping_agent/tools.py - Shopping Agent implementation
  • samples/python/src/roles/merchant_agent/tools.py - Merchant Agent implementation
  • samples/go/pkg/ap2/types/mandate.go - Go type definitions
  • docs/specification.md - Specification documentation

References

  • Specification: docs/specification.md
  • Type definitions: src/ap2/types/mandate.py
  • Shopping Agent: samples/python/src/roles/shopping_agent/tools.py
  • Merchant Agent: samples/python/src/roles/merchant_agent/tools.py

Notes

This issue is not meant to criticize the current work, but to ensure that when developers use this repository as a reference, they understand the gaps and implement the necessary security mechanisms. The specification documents are excellent and well-designed; the issue is ensuring the code implementation matches the security requirements specified.

The repository is extremely valuable as a learning resource and concept validation tool. The goal is to make it even better by aligning implementation with specification.

Correct Understanding: The current implementation correctly includes the Cart Mandate in the user's signature through cart_mandate_hash. Since cart_mandate_hash is computed from the complete Cart Mandate object (including the merchant's signature), the user's signature indirectly binds to the merchant-signed Cart Mandate. The primary security gap is the missing Intent Mandate binding in Human Not Present scenarios.

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions