اتصال PHP به سامانه مودیان (TP Tax) — راهنمای کامل پیادهسازی احراز هویت، امضای JWS، رمزنگاری JWE، ارسال صورتحساب و استعلام وضعیت (با نمونهکد PHP)
ساختار کلی فرایند (خلاصهٔ مراحل)
1- دریافت nonce از سرور (GET /api/v2/nonce).
2- ساختن Payload شامل nonce و clientId.
3- تولید JWS (توکن) با الگوریتم RS256؛ Header باید شامل x5c (زنجیرهٔ گواهی Base64)، sigT (زمان امضا ISO-8601 UTC)، typ: "jose", crit: ["sigT"], و cty: "text/plain". این JWS بهعنوان توکن احراز هویت در هدر Authorization: Bearer ارسال میشود.
4- با توکن فوق، فراخوانی GET /api/v2/server-information برای دریافت publicKeys (کلید عمومی سرور برای رمزنگاری JWE).
5- ساخت صورتحساب JSON (header/body/payments) — امضا بهصورت JWS (مشابه توکن) — سپس رمزنگاری JWE (alg: RSA-OAEP-256, enc: A256GCM, kid: server key id).
6- ارسال بسته JWE در بدنهٔ POST /api/v2/invoice به شکل آرایهای از InvoicePacket شامل payload (رشتهٔ JWE) و header (requestTraceId و fiscalId).
7- دریافت نتیجه شامل uid و referenceNumber و در ادامه استعلام وضعیت با endpoints مناسب (/inquiry-by-uid, /inquiry-by-reference-id, یا /inquiry).
پیشنیازها و کتابخانهها (PHP)
-
PHP 8+
-
توابع OpenSSL (openssl_* در PHP)
-
composer packageها (مثلاً):
-
firebase/php-jwt— برای ساخت JWS (یا میتوانید با openssl دستی بسازید). -
phpseclib/phpseclib— برای کارهای کلید و رمزنگاری در صورت نیاز.
-
نمونه نصب:
composer require firebase/php-jwt phpseclib/phpseclib
بخش A — توابع کمکی (Base64URL و UUID و بارگذاری گواهی)
<?php
function base64url_encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function base64url_decode($data) {
$remainder = strlen($data) % 4;
if ($remainder) $data .= str_repeat('=', 4 - $remainder);
return base64_decode(strtr($data, '-_', '+/'));
}
function gen_uuid_v4() {
$data = random_bytes(16);
$data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
$data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
بخش B — دریافت Nonce
طبق سند: GET https://tp.tax.gov.ir/requestsmanager/api/v2/nonce?timeToLive=20 خروجی شامل nonce و expDate است.
function get_nonce($ttl = 20) {
$url = "https://tp.tax.gov.ir/requestsmanager/api/v2/nonce?timeToLive=" . intval($ttl);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$resp = curl_exec($ch);
if ($resp === false) throw new Exception(curl_error($ch));
$obj = json_decode($resp, true);
if (!$obj || !isset($obj['nonce'])) throw new Exception("Invalid nonce response");
return $obj;
}
بخش C — ساخت JWS (برای احراز هویت): تولید JWT امضا شده با RS256 و x5c در هدر
سند بیان میکند header باید شامل x5c (certificate chain) و sigT باشد و امضا RS256 باشد.
در PHP دو راه هست: (1) استفاده از firebase/php-jwt و درج هدر سفارشی، یا (2) ساخت دستی بخشهای Base64URL و امضا با openssl_sign (پیشنهاد میکنم برای کنترل کامل، روش دستی را ببینید).
روش دستی (برای شفافیت)
function create_jws_rs256($payload_array, $privateKeyPemPath, $certPemPath) {
$payload = json_encode($payload_array, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// load cert and private key
$cert = file_get_contents($certPemPath);
$cert_der = pem_to_der($cert); // تابع کمکی پایین
$cert_b64 = base64_encode($cert_der);
$sigT = gmdate("Y-m-d\\TH:i:s\\Z"); // ISO-8601 UTC
$header = [
"alg" => "RS256",
"x5c" => [$cert_b64],
"sigT" => $sigT,
"typ" => "jose",
"crit" => ["sigT"],
"cty" => "text/plain"
];
$protected = base64url_encode(json_encode($header, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$b64payload = base64url_encode($payload);
$signingInput = $protected . "." . $b64payload;
$privateKey = file_get_contents($privateKeyPemPath);
$pkeyid = openssl_pkey_get_private($privateKey);
if (!$pkeyid) throw new Exception("Invalid private key");
$signature = '';
$ok = openssl_sign($signingInput, $signature, $pkeyid, OPENSSL_ALGO_SHA256);
openssl_free_key($pkeyid);
if (!$ok) throw new Exception("Signing failed");
$b64sig = base64url_encode($signature);
return $protected . "." . $b64payload . "." . $b64sig;
}
function pem_to_der($pem) {
$pem = trim($pem);
$lines = explode("\n", $pem);
$data = '';
foreach ($lines as $line) {
if (strpos($line, '-----') === 0) continue;
$data .= trim($line);
}
return base64_decode($data);
}
توجه: clientId در payload باید شناسهٔ حافظهٔ مالیاتی (fiscalId / clientId) باشد (مقداری که سازمان به شما تخصیص داده)
بخش D — درخواست به سرور اطلاعات (با Authorization: Bearer )
پس از تولید JWS از بالا، باید آن را در هدر Authorization قرار دهی و اطلاعات سرور (شامل publicKeys) را دریافت کنی. مثال (curl):
$jwt = create_jws_rs256(["nonce"=>$nonceVal,"clientId"=>$clientId], "private_key.pem", "certificate.crt");
$ch = curl_init("https://tp.tax.gov.ir/requestsmanager/api/v2/server-information");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $jwt",
"accept: */*"
]);
$resp = curl_exec($ch);
$data = json_decode($resp, true);
پاسخ شامل serverTime و آرایهای publicKeys است که هر عنصر key (Base64)، id, algorithm, purpose دارد. از این key برای رمزنگاری JWE استفاده میشود.
بخش E — ساخت JWS برای صورتحساب (Invoice JWS)
صورتحساب باید بهصورت JSON (مثال قالب در سند) ساخته شود و سپس همان روش JWS برای امضای آن اعمال شود (Header مشابه اما cty ممکن است text/plain و payload = JSON صورتحساب باشد). سند نمونهٔ payload صورتحساب را ارائه داده است.
$invoiceJson = json_encode($invoiceArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$jws_invoice = create_jws_rs256(json_decode($invoiceJson, true), "private_key.pem", "certificate.crt");
// note: create_jws_rs256 above expects array; adapt if needed
(در عمل ممکن بخواهید تابعی که payload را از آرایه میگیرد مستقیماً استفاده کنید.)
بخش F — تبدیل JWS به JWE (رمزنگاری بستهٔ صورتحساب)
سند شرح میدهد که قالب JWE مطابق RFC7516 است و Header JWE باید چیزی شبیه به:
{"alg":"RSA-OAEP-256","enc":"A256GCM","kid":""}
و ساختار رشتهٔ JWE به صورت:
Base64URL(JWE Protected Header) || '.' ||
Base64URL(JWE Encrypted Key) || '.' ||
Base64URL(JWE Initialization Vector) || '.' ||
Base64URL(JWE Ciphertext) || '.' ||
Base64URL(JWE Authentication Tag)
``` :contentReference[oaicite:14]{index=14}
### پیادهسازی در PHP — گام به گام
1. تولید یک کلید متقارن CEK (Content Encryption Key) 32 بایت برای AES-256-GCM.
2. Encrypt کردن CEK با کلید عمومی سرور (از `publicKeys[].key` که Base64 است) با RSA-OAEP-256 (RSAES-OAEP with SHA-256).
3. تولید IV (96-bit recommended) برای AES-GCM.
4. محاسبه `aad` = Base64URL(Protected Header).
5. AES-256-GCM بر روی plaintext = JWS_invoice برای تولید `ciphertext` و `tag`.
6. ساخت قطعهٔ JWE با Base64URL همهٔ قسمتها.
**مثال (تابع سادهشده):**
```php
function create_jwe_rsa_oaep_a256gcm($jws_plaintext, $serverPublicKeyBase64, $kid) {
// 1. protected header
$header = ["alg"=>"RSA-OAEP-256","enc"=>"A256GCM","kid"=>$kid];
$protected = base64url_encode(json_encode($header, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// 2. generate CEK (32 bytes)
$cek = random_bytes(32);
// 3. encrypt CEK with server public key (RSA-OAEP with SHA-256)
$serverPubDer = base64_decode($serverPublicKeyBase64);
// convert DER to PEM
$serverPubPem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($serverPubDer),64,"\n") . "-----END PUBLIC KEY-----\n";
$encryptedKey = '';
$ok = openssl_public_encrypt($cek, $encryptedKey, $serverPubPem, OPENSSL_PKCS1_OAEP_PADDING);
// Note: PHP's openssl_public_encrypt uses OAEP with SHA1 by default; RSA-OAEP-256 (SHA-256) is not directly supported by openssl_public_encrypt.
// For RSA-OAEP-256 you may need ext/openssl 1.1.1+ with openssl_public_encrypt using OPENSSL_PKCS1_OAEP_PADDING and an option for SHA-256 isn't available in PHP < 8.1.
// A robust solution uses phpseclib for RSA-OAEP-256. (See note below.)
if (!$ok) throw new Exception("encrypt CEK failed");
// 4. iv
$iv = random_bytes(12); // 96-bit
// 5. AES-256-GCM encrypt
$ciphertext = openssl_encrypt($jws_plaintext, 'aes-256-gcm', $cek, OPENSSL_RAW_DATA, $iv, $tag, $protected);
if ($ciphertext === false) throw new Exception("AES-GCM failed");
return implode('.', [
$protected,
base64url_encode($encryptedKey),
base64url_encode($iv),
base64url_encode($ciphertext),
base64url_encode($tag)
]);
}
Response شامل timestamp و result[] با uid و referenceNumber است.
نمونهٔ ارسال:
$invoicePacket = [
"payload" => $jwe_string,
"header" => [
"requestTraceId" => gen_uuid_v4(),
"fiscalId" => $fiscalId
]
];
$body = json_encode([$invoicePacket], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$ch = curl_init("https://tp.tax.gov.ir/requestsmanager/api/v2/invoice");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $jwt", // jwt generated earlier for auth
"Content-Type: application/json",
"Accept: */*"
]);
$response = curl_exec($ch);
$result = json_decode($response, true);
بخش H — استعلام وضعیت صورتحسابها
پس از ارسال، میتوان وضعیت را با استفاده از inquiry-by-reference-id یا inquiry-by-uid یا inquiry (بر اساس بازه زمانی) بررسی کرد. نمونهٔ endpointها و پارامترها در سند آمدهاند.
مثال استعلام با UID:
$url = "https://tp.tax.gov.ir/requestsmanager/api/v2/inquiry-by-uid?uidList=" . urlencode($uid) . "&fiscalId=" . urlencode($fiscalId);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $jwt"]);
$resp = curl_exec($ch);
بخش I — نکات امنیتی، عملی و مسائل رایج
-
کلیدها و گواهیها: کلید خصوصی را همیشه امن نگهدار، دسترسی فایل را محدود کن (مثلاً 600). در سند تاکید شده که امضاکننده باید دارای گواهی معتبر از مراجع مورد قبول باشد.
- امضای JWT و زمان sigT:
sigTباید به فرمت ISO_8601 UTC (yyyy-MM-dd'T'HH:mm:ss'Z') باشد و در هدر JWS ارسال شود. سند این مورد را مشخص کرده است. - RSA-OAEP-256: برای رمزنگاری CEK با RSA-OAEP-256 از کتابخانهای که صراحتاً SHA-256 OAEP را پشتیبانی کند استفاده کن (مثلاً
phpseclib). استفاده از OAEP-SHA1 اشتباه خواهد بود اگر سرور صراحتاً OAEP-256 بخواهد. - تست مرحلهبهمرحله: ابتدا فقط احراز هویت (ساخت JWS و GET server-information) را تست کن، سپس امضای صورتحساب (JWS) را تست، و در آخر JWE و ارسال. این تقسیمبندی خطاها را کم میکند.
پیشنهاد میکنم این الگو را بهعنوان نقطهٔ شروع استفاده کنید؛ در محیط واقعی با مدیریت خطا و لاگ دقیق، بررسی پاسخ گواهیها و OCSP/CRL و مدیریت کلیدها تکمیل کنید.
توجه: بهدلایل طول متن، کد بصورت خلاصه شده توضیح داده شده و در صورت نیاز، میتوانید با پشتیبانی بودجکس تماس بگیرید.
سخن پایانی: سعی شده با ساده ترین حالت این راهنمای پیاده سازی در بودجکس ارائه شود تا برنامه نویسان و توسعه دهندگان در پروژه های خود استفاده کنند.
همچنین در صورت نیاز به خدمات و طراحی و توسعه اپلیکیشن و نرم افزار و همکاری در پروژه های مشابه میتوانید با ما در تماس باشید.



