Invalid request when trying to use my CSR in compliance

Have you successfully created CSR without OpenSSL Command in PHP?

I succeeded in Python code, for PHP code always fails to add extension [alt_names] to CSR.

Please share your code if you succeed in doing it in PHP.

Thanks

Just tried again and it worked

CsrGenerator.php

<?php

class CsrGenerator
{
    private $config;
    private $environment_type;
    private $asn_template;

    public function __construct($config, $environment_type)
    {
        $this->config = $config;
        $this->environment_type = $environment_type;
        $this->asn_template = $this->getAsnTemplate();
    }

    private function getAsnTemplate()
    {
        if ($this->environment_type == 'NonProduction') {
            return 'TSTZATCA-Code-Signing';
        } elseif ($this->environment_type == 'Simulation') {
            return 'PREZATCA-Code-Signing';
        } elseif ($this->environment_type == 'Production') {
            return 'ZATCA-Code-Signing';
        } else {
            throw new Exception("Invalid environment type specified.");
        }
    }

    // Membuat konfigurasi .cnf dinamis berdasarkan input
    private function generateConfigFile()
    {
        $config = $this->config;
        $cnfContent = "
oid_section = OIDs
[OIDs]
certificateTemplateName=1.3.6.1.4.1.1311.20.2

[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[dn]
CN={$config['csr.common.name']}
OU={$config['csr.organization.unit.name']}
O={$config['csr.organization.name']}
C={$config['csr.country.name']}

[req_ext]
certificateTemplateName = ASN1:PRINTABLESTRING:{$this->asn_template}
subjectAltName = dirName:alt_names

[alt_names]
SN={$config['csr.serial.number']}
UID={$config['csr.organization.identifier']}  # Default from your config
title={$config['csr.invoice.type']}  # Default from your config
registeredAddress={$config['csr.location.address']}  # Default from your config
businessCategory={$config['csr.industry.business.category']}
";


        // Menyimpan konten ke file config.cnf
        $filePath = 'config.cnf';
        file_put_contents($filePath, $cnfContent);

        return $filePath;
    }

    public function generatePrivateKey()
    {
        // Menggunakan SECP256K1 untuk private key
        $privateKey = openssl_pkey_new([
            'private_key_type' => OPENSSL_KEYTYPE_EC,
            'curve_name' => 'secp256k1', // Menggunakan SECP256K1 sesuai dengan permintaan
        ]);

        return $privateKey;
    }

    public function generateCsr()
    {
        // Menyusun file konfigurasi OpenSSL yang dinamis
        $configFile = $this->generateConfigFile();

        // Membuat private key
        $privateKey = $this->generatePrivateKey();

        // Membuat DN untuk CSR
        $dn = [
            "countryName" => $this->config['csr.country.name'] ?? 'SA',
            "organizationalUnitName" => $this->config['csr.organization.unit.name'] ?? '',
            "organizationName" => $this->config['csr.organization.name'] ?? '',
            "commonName" => $this->config['csr.common.name'] ?? '',
        ];

        // Pastikan untuk menyertakan file konfigurasi yang benar dalam csrConfig
        $csrConfig = [
            "config" => $configFile, // Menyertakan file konfigurasi yang telah dibuat
            "digest_alg" => "sha256",
        ];

        // Buat CSR menggunakan konfigurasi yang sudah diubah
        $csr = openssl_csr_new($dn, $privateKey, $csrConfig);

        if (!$csr) {
            throw new Exception('Error generating CSR: ' . openssl_error_string());
        }

        // Menandatangani CSR
        $csrPem = '';
        if (!openssl_csr_sign($csr, null, $privateKey, 365, ['digest_alg' => 'sha256'])) {
            throw new Exception('Error signing CSR: ' . openssl_error_string());
        }

        // Menyimpan private key dan CSR ke dalam format PEM
        openssl_pkey_export($privateKey, $privateKeyPem);
        openssl_csr_export($csr, $csrPem);

        // Strip header/footer dari private key
        $privateKeyContent = preg_replace('/-+BEGIN[^-]+-+|-+END[^-]+-+/', '', $privateKeyPem);  
        $privateKeyContent = str_replace(["\r", "\n"], '', $privateKeyContent); 

        // Encode CSR dalam Base64
        $csrBase64 = base64_encode($csrPem);

        return [$privateKeyContent, $csrBase64];
    }

    public function saveToFiles($privateKeyPem, $csrPem)
    {
        if (!file_exists('certificates')) {
            if (!mkdir('certificates', 0777, true)) {
                throw new Exception('Failed to create certificates directory.');
            }
        }

        file_put_contents('certificates/PrivateKey.pem', $privateKeyPem);
        file_put_contents('certificates/taxpayer.csr', $csrPem);

        echo "\nPrivate key and CSR have been saved to certificates/PrivateKey.pem and certificates/taxpayer.csr, respectively.\n";
    }
}

.
CsrGenerator_test.php

<?php
require("Helpers/CsrGenerator.php");

$config = [
    "csr.common.name" => "TST-886431145-399999999900003",
    "csr.serial.number" => "1-TST|2-TST|3-ed22f1d8-e6a2-1118-9b58-d9a8f11e445f",
    "csr.organization.identifier" => "399999999900003",
    "csr.organization.unit.name" => "Riyadh Branch",
    "csr.organization.name" => "Maximum Speed Tech Supply LTD",
    "csr.country.name" => "SA",
    "csr.invoice.type" => "1100",
    "csr.location.address" => "RRRD2929",
    "csr.industry.business.category" => "Supply activities"
];

$environmentType = "NonProduction";

// Instantiate CSR Generator
$csrGen = new CsrGenerator($config, $environmentType);
list($privateKeyContent, $csrBase64) = $csrGen->generateCsr();

echo "Private Key (without header and footer):\n";
echo $privateKeyContent . "\n\n";
echo "Base64 Encoded CSR:\n";
echo $csrBase64 . "\n\n";

// Define ZATCA endpoint and OTP
$otp = '123456';
$url = "https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal/compliance";

// Prepare JSON payload
$jsonPayload = json_encode([
    'csr' => $csrBase64
]);

// Set cURL options
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'accept: application/json',
    'accept-language: en',
    'OTP: ' . $otp,
    'Accept-Version: V2',
    'Content-Type: application/json'
]);

// Execute cURL and get the response
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

// Output server response
if ($httpCode === 200) {
    echo "\n\nServer Response: \n" . json_encode(json_decode($response), JSON_PRETTY_PRINT);
} else {
    echo "\n\nServer Response: \n" . $response;
    if ($response === false) {
        echo "cURL error: " . curl_error($ch);
    }
}
?>

.
Result


Private Key (without header and footer):
MHQCAQEEID9l9tzL6GHLkNR0JHFciG/PCrSTpq7OynUHwxrr50KxoAcGBSuBBAAKoUQDQgAE9wCiRXFWxtvlc92tKCNNpOHusjiyNXSIeBm+1fJuf5pbebgUVUL/Fc1/w2pwShbHjPKUDjyzJPI9A8qlryj7+Q==

Base64 Encoded CSR:
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ0dqQ0NBYjhDQVFBd2RURUxNQWtHQTFVRUJoTUNVMEV4RmpBVUJnTlZCQXNNRFZKcGVXRmthQ0JDY21GdQpZMmd4SmpBa0JnTlZCQW9NSFUxaGVHbHRkVzBnVTNCbFpXUWdWR1ZqYUNCVGRYQndiSGtnVEZSRU1TWXdKQVlEClZRUUREQjFVVTFRdE9EZzJORE14TVRRMUxUTTVPVGs1T1RrNU9Ua3dNREF3TXpCV01CQUdCeXFHU000OUFnRUcKQlN1QkJBQUtBMElBQlBjQW9rVnhWc2JiNVhQZHJTZ2pUYVRoN3JJNHNqVjBpSGdadnRYeWJuK2FXM200RkZWQwoveFhOZjhOcWNFb1d4NHp5bEE0OHN5VHlQUVBLcGE4bysvbWdnZW93Z2VjR0NTcUdTSWIzRFFFSkRqR0IyVENCCjFqQWtCZ2tyQmdFRUFZb2ZGQUlFRnhNVlZGTlVXa0ZVUTBFdFEyOWtaUzFUYVdkdWFXNW5NSUd0QmdOVkhSRUUKZ2FVd2dhS2tnWjh3Z1p3eE96QTVCZ05WQkFRTU1qRXRWRk5VZkRJdFZGTlVmRE10WldReU1tWXhaRGd0WlRaaApNaTB4TVRFNExUbGlOVGd0WkRsaE9HWXhNV1UwTkRWbU1SOHdIUVlLQ1pJbWlaUHlMR1FCQVF3UE16azVPVGs1Ck9UazVPVEF3TURBek1RMHdDd1lEVlFRTURBUXhNVEF3TVJFd0R3WURWUVFhREFoU1VsSkVNamt5T1RFYU1CZ0cKQTFVRUR3d1JVM1Z3Y0d4NUlHRmpkR2wyYVhScFpYTXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBS0ZqWHdrNgpKMFpKUkRQU3hjSjN0T0ZsaURvM1Bmb21WRWFER3IzMDNYcDhBaUVBOUoyTWc0NURhSEtnb3hLdUhQU1RoNkVQCkhhckM5bmRGemZiTGRDT0pzcnc9Ci0tLS0tRU5EIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLQo=

Server Response: 
{
    "requestID": 1234567890123,
    "dispositionMessage": "ISSUED",
    "binarySecurityToken": "TUlJQ1BUQ0NBZU9nQXdJQkFnSUdBWk1NWjB1SU1Bb0dDQ3FHU000OUJBTUNNQlV4RXpBUkJnTlZCQU1NQ21WSmJuWnZhV05wYm1jd0hoY05NalF4TVRBNE1UVXpNVFV4V2hjTk1qa3hNVEEzTWpFd01EQXdXakIxTVFzd0NRWURWUVFHRXdKVFFURVdNQlFHQTFVRUN3d05VbWw1WVdSb0lFSnlZVzVqYURFbU1DUUdBMVVFQ2d3ZFRXRjRhVzExYlNCVGNHVmxaQ0JVWldOb0lGTjFjSEJzZVNCTVZFUXhKakFrQmdOVkJBTU1IVlJUVkMwNE9EWTBNekV4TkRVdE16azVPVGs1T1RrNU9UQXdNREF6TUZZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUFvRFFnQUU5d0NpUlhGV3h0dmxjOTJ0S0NOTnBPSHVzaml5TlhTSWVCbSsxZkp1ZjVwYmViZ1VWVUwvRmMxL3cycHdTaGJIalBLVURqeXpKUEk5QThxbHJ5ajcrYU9Cd1RDQnZqQU1CZ05WSFJNQkFmOEVBakFBTUlHdEJnTlZIUkVFZ2FVd2dhS2tnWjh3Z1p3eE96QTVCZ05WQkFRTU1qRXRWRk5VZkRJdFZGTlVmRE10WldReU1tWXhaRGd0WlRaaE1pMHhNVEU0TFRsaU5UZ3RaRGxoT0dZeE1XVTBORFZtTVI4d0hRWUtDWkltaVpQeUxHUUJBUXdQTXprNU9UazVPVGs1T1RBd01EQXpNUTB3Q3dZRFZRUU1EQVF4TVRBd01SRXdEd1lEVlFRYURBaFNVbEpFTWpreU9URWFNQmdHQTFVRUR3d1JVM1Z3Y0d4NUlHRmpkR2wyYVhScFpYTXdDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWhBT2NhUk83aHNXM0FSZkpFK3J6UzErZExmY2dqOVBSTzNJd3V1RG1wZDc0V0FpQVdyRE5jQlZ0eDE3bytqN2o0alNMaDEvYnNJSHFNUWUwdVJvNC80MGFDRmc9PQ==",
    "secret": "fwHuYqxG\/h+YnAuB1oPX\/Wzw3I9YZPzSXS1mqPxwQ3U=",
    "errors": null
}

Hope this help