diff --git a/src/Services/OpenSslCmsSigner.php b/src/Services/OpenSslCmsSigner.php index d0438c8..02f8e6d 100644 --- a/src/Services/OpenSslCmsSigner.php +++ b/src/Services/OpenSslCmsSigner.php @@ -1,47 +1,33 @@ -// use Apoloz\AlfaBank\Contracts\SignerInterface; use Apoloz\AlfaBank\Exceptions\SignerException; -// - -//
-// class OpenSslCmsSigner implements SignerInterface { - //
- // public function __construct( protected string $certificatePath, protected string $privateKeyPath ) {} - //
- //
- // public function sign(string $data): string { - $privateKey = openssl_pkey_get_private(file_get_contents($this->privateKeyPath)); // assuming no passphrase for simplicity here - if (! $privateKey) { - throw new SignerException("Could not read private key for signing from {$this->privateKeyPath}."); + if (!file_exists($this->privateKeyPath)) { + throw new SignerException("Private key file not found: {$this->privateKeyPath}"); + } + if (!file_exists($this->certificatePath)) { + throw new SignerException("Certificate file not found: {$this->certificatePath}"); } + $privateKey = openssl_pkey_get_private(file_get_contents($this->privateKeyPath)); $cert = openssl_x509_read(file_get_contents($this->certificatePath)); - if (! $cert) { - throw new SignerException("Could not read certificate for signing from {$this->certificatePath}."); + + if (!$privateKey || !$cert) { + throw new SignerException("Could not read private key or certificate for signing."); } $inFile = tempnam(sys_get_temp_dir(), 'alfa_in'); @@ -49,34 +35,35 @@ class OpenSslCmsSigner implements SignerInterface file_put_contents($inFile, $data); + // Используем PKCS7_ATTACHED согласно документации Альфа-Банка $status = openssl_pkcs7_sign( $inFile, $outFile, $cert, $privateKey, [], - PKCS7_BINARY | PKCS7_DETACHED + PKCS7_BINARY // По умолчанию это ATTACHED, если не передан PKCS7_DETACHED ); - if (! $status) { - if (file_exists($inFile)) { - unlink($inFile); - } - if (file_exists($outFile)) { - unlink($outFile); - } - throw new SignerException('Signing failed: ' . openssl_error_string()); + if (!$status) { + @unlink($inFile); + @unlink($outFile); + throw new SignerException("OpenSSL sign failed: " . openssl_error_string()); } $signedData = file_get_contents($outFile); - unlink($inFile); - unlink($outFile); + @unlink($inFile); + @unlink($outFile); - $parts = explode("\n\n", $signedData); - $signature = end($parts); + // Извлекаем Base64 тело подписи согласно примеру из документации Альфы + $parts = explode("\n\n", $signedData, 2); + if (isset($parts[1])) { + $signature = trim($parts[1]); + // Удаляем возможные PEM-завершители и переносы строк + $signature = str_replace(["-----BEGIN PKCS7-----", "-----END PKCS7-----", "\n", "\r", " "], '', $signature); + return $signature; + } - return str_replace(["\n", "\r"], '', $signature); + throw new SignerException("Failed to extract signature body from CMS container."); } - //
} -//
diff --git a/src/Traits/SignableData.php b/src/Traits/SignableData.php index c718b54..0b698f1 100644 --- a/src/Traits/SignableData.php +++ b/src/Traits/SignableData.php @@ -12,37 +12,47 @@ trait SignableData { public function sign(SignerInterface $signer): self { - // 1. Create a clone to represent the data-to-be-signed (without existing signatures) - $clone = clone $this; + // 1. Get raw data as array + $data = $this->toArray(); - // We explicitly set digestSignatures to null or empty for the signature calculation - // assuming the bank expects the "business payload" only. - // If the bank expects the field "digestSignatures": [] to be present in signed string, - // we should initialize it as empty collection. - // Based on common practices, we exclude it or keep it null. - // Let's assume we exclude it (set to null if the property is nullable). - $clone->digestSignatures = null; + // 2. Remove existing signatures if any + unset($data['digestSignatures']); - // 2. Serialize to JSON - // We use JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES to match typical API expectations - $jsonToSign = $clone->toJson(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + // 3. Canonicalize (RFC 8785 - simple version: sort keys, remove nulls, no spaces) + $canonicalData = $this->canonicalizeArray($data); + + $jsonToSign = json_encode($canonicalData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - // 3. Sign + // 4. Sign $signature = $signer->sign($jsonToSign); - // 4. Attach signature to the original object + // 5. Attach signature to the original object if ($this->digestSignatures === null) { $this->digestSignatures = new DataCollection(DigestSignatureData::class, []); } - // We can't push to DataCollection directly if it's not array-access compatible in all versions, - // but Spatie DataCollection is usually iterable. - // Safest way is to reconstruct it. $current = array_values($this->digestSignatures->toCollection()->all()); - $current[] = new DigestSignatureData($signature, 'cms'); + $current[] = new DigestSignatureData($signature, 'rsa'); $this->digestSignatures = new DataCollection(DigestSignatureData::class, $current); return $this; } + + private function canonicalizeArray(array $array): array + { + // Remove nulls and empty strings + $array = array_filter($array, fn($v) => $v !== null); + + // Sort keys lexically + ksort($array); + + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = $this->canonicalizeArray($value); + } + } + + return $array; + } }