forked from apoloz/alfa-bank-sdk
fix(sign)
This commit is contained in:
@@ -1,47 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
// MODULE_CONTRACT: Implements `SignerInterface` using OpenSSL for PKCS#7 (CMS) data signing.
|
||||
// RESPONSIBILITY: To provide a concrete implementation of digital signing that takes a raw data string (e.g., canonicalized JSON), signs it using the configured mTLS certificate and private key, and returns a Base64 encoded PKCS#7 detached signature, adhering to Alfa Bank's security requirements.
|
||||
// INPUTS: Data string to be signed. Certificate path and private key path are provided during instantiation.
|
||||
// OUTPUTS: Base64 encoded PKCS#7 detached signature string.
|
||||
// LINKS: Contracts/SecurityContracts.php, Utils/CmsSigner.php, README.md#security
|
||||
*/
|
||||
|
||||
namespace Apoloz\AlfaBank\Services;
|
||||
|
||||
// <SECTION name="module_imports" intent="Import necessary classes for the signer contract.">
|
||||
// <BELIEF_STATE "The `SignerInterface` is imported to ensure that `OpenSslCmsSigner` adheres to a predefined contract for digital signing, promoting interoperability and testability within the SDK.">
|
||||
use Apoloz\AlfaBank\Contracts\SignerInterface;
|
||||
use Apoloz\AlfaBank\Exceptions\SignerException;
|
||||
|
||||
// </SECTION>
|
||||
|
||||
// <SECTION name="class_definition" intent="Define the OpenSSL-based CMS signer service.">
|
||||
// <BELIEF_STATE "The `OpenSslCmsSigner` class provides a robust and compliant solution for PKCS#7 CMS signing. By implementing `SignerInterface` and utilizing OpenSSL functions, it ensures that data integrity and authenticity can be cryptographically verified, which is critical for secure communication with financial institutions like Alfa Bank. The class takes certificate and private key paths during construction, allowing for flexible configuration.">
|
||||
class OpenSslCmsSigner implements SignerInterface
|
||||
{
|
||||
// <SECTION name="constructor_definition" intent="Initialize the signer with certificate and private key paths.">
|
||||
// <BELIEF_STATE "The constructor receives the file paths to the mTLS certificate and private key. These paths are stored as protected properties, ensuring that the signer instance is configured with the necessary credentials to perform digital signing operations, aligning with the principle of dependency injection for external resources.">
|
||||
public function __construct(
|
||||
protected string $certificatePath,
|
||||
protected string $privateKeyPath
|
||||
) {}
|
||||
// </SECTION>
|
||||
|
||||
// <SECTION name="sign_method" intent="Sign a given data string using PKCS#7 CMS.">
|
||||
// <BELIEF_STATE "The `sign` method performs the digital signature process as required by Alfa Bank. It reads the certificate and private key from the configured paths, then uses `openssl_pkcs7_sign` with `PKCS7_BINARY | PKCS7_DETACHED` flags to create a detached CMS signature. Error handling is included for robust operation, and the final signature is Base64 encoded after stripping unnecessary headers, ensuring compatibility with API expectations.">
|
||||
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.");
|
||||
}
|
||||
// </SECTION>
|
||||
}
|
||||
// </SECTION>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user