The Comfino REST API requires cryptographic request signing for order creation to ensure data integrity and prevent tampering. This document explains the signing mechanism and how the client handles it automatically.
When you create or validate an order, the client automatically computes three security headers:
Comfino-Cart-Hash— SHA3-256 hash of cart dataComfino-Customer-Hash— SHA3-256 hash of customer dataComfino-Order-Signature— SHA3-256 HMAC of cart + customer hashes
These headers allow the server to verify that:
- Cart and customer data hasn't been tampered with in transit
- The request originates from an authenticated shop (only the shop knows the API key)
use Comfino\Api\Client;
use Comfino\Http\GuzzleHttpClient;
$client = new Client(
apiKey: 'your-api-key',
httpClient: new GuzzleHttpClient(),
);use Comfino\Api\Request\CreateOrder;
use Comfino\Shop\Order\Order;
use Comfino\Shop\Order\Cart;
use Comfino\Shop\Order\Customer;
$order = new Order(
orderId: '12345',
cart: new Cart(...), // See below
customer: new Customer(...), // See below
);
// The client automatically computes and injects the three security headers.
try {
$response = $client->createOrder($order);
// Order created successfully.
} catch (RequestValidationError $e) {
// Cart or customer data didn't pass validation.
}Cart Hash:
SHA3-256(JSON-serialized cart data)
The cart is serialized to JSON (PSR-12 style, consistent), then hashed with SHA3-256. Example:
{
"description": "Wireless headphones x2",
"totalAmount": 19999,
"deliveryAddress": { ... },
"itemsCount": 2
}
Customer Hash:
SHA3-256(JSON-serialized customer data)
Similarly for customer.
Order Signature:
SHA3-256_HMAC(cartHash + customerHash, apiKey)
The two hashes are concatenated (no separator), then an HMAC-SHA3-256 is computed with your API key as the secret. This proves the shop (who possesses the API key) authorized the order.
CreateOrder extends Request and implements the signing logic in its constructor:
class CreateOrder extends Request
{
public function __construct(OrderInterface $order)
{
parent::__construct(
method: 'POST',
path: '/orders',
body: $order // Serialized to JSON in base Request.
);
// Compute hashes from the order's cart and customer.
$cartHash = hash('sha3-256', json_encode($order->getCart(), self::JSON_FLAGS));
$customerHash = hash('sha3-256', json_encode($order->getCustomer(), self::JSON_FLAGS));
// Combine hashes and compute signature.
$signature = hash_hmac(
'sha3-256',
$cartHash . $customerHash,
$apiKey, // Passed from client
false // Return hex string
);
// Inject security headers.
$this->setHeader('Comfino-Cart-Hash', $cartHash);
$this->setHeader('Comfino-Customer-Hash', $customerHash);
$this->setHeader('Comfino-Order-Signature', $signature);
}
}- Algorithm: SHA3-256 (requires
ext-sodiumorext-hash). - Integrity: Prevents man-in-the-middle attacks (attacker cannot modify cart data without knowing the API key).
- Authentication: Proves the request came from the shop (API key holder).
- Timing safety: Not applicable here; server uses simple comparison (no secrets are verified by client).
Hashes must match on both client and server, so serialization must be byte-for-byte identical.
The client uses:
json_encode(
$data,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK
);Ensure your Cart and Customer implementations serialize to the same JSON format. The CartTrait provided in this library handles this automatically.
Hidden fields and data validation
Do not add extra fields to Cart or Customer objects at runtime:
// BAD — modifies data after serialization.
$cart = new Cart(...);
$cart->extraField = 'something'; // This will hash differently!Instead, pass all data in the constructor:
// GOOD — all data known upfront.
$cart = new Cart(
description: 'Item 1 + Item 2',
totalAmount: 9999,
// ... other fields
);If you rotate API keys:
- Generate a new key in the Comfino dashboard.
- Update your shop's configuration.
- Incoming requests with the old key are rejected server-side.
The client doesn't cache API keys — each request uses the currently configured key.
Use different API keys for sandbox and production:
// Development
$sandboxClient = new Client(
apiKey: 'sandbox_key_...',
sandbox: true,
);
// Production
$prodClient = new Client(
apiKey: 'live_key_...',
sandbox: false,
);The signing algorithm is identical; only the API key and endpoint URL differ.
For the opposite direction (server to client), see webhook-verification.md — the server sends a CR-Signature header that you verify using WebhookSignatureVerifier.
When testing order creation, use fixtures with known hashes:
use Comfino\Tests\Fixture\OrderFixture;
$order = OrderFixture::createOrder();
// Hash computation is tested separately; focus on business logic.The server computed a different cart hash than your client sent. Likely causes:
- Cart data changed after request preparation — Ensure all data is finalized before calling
createOrder(). - JSON serialization differs — Verify
Cart::jsonSerialize()matches the expected format. - API key mismatch — Confirm you're using the correct sandbox/production key.
Fix:
// Verify the order data before sending.
echo json_encode($order->getCart(), JSON_PRETTY_PRINT);
// Check API key configuration.
echo $client->getApiKey(); // Should not be empty/default.The server couldn't verify the HMAC signature. Possible causes:
- API key changed mid-request — Don't update the client's API key between request preparation and sending.
- Request body modified in transit — Rare; indicates network layer tampering or a middleware issue.
Fix: Check that your HTTP client isn't modifying request headers or body.
- Sodium extension documentation — for cryptographic operations
- Comfino API documentation — Server-side signature validation details