Cómo Firmar PDFs Usando Certificados Digitales en Laravel, PHP y Symfony
Introducción
En esta guía aprenderás a firmar PDFs digitalmente usando PHP, Laravel y Symfony. Para simplificar el proceso, usaremos pdfsignify.com, una API que permite enviar un PDF, un certificado digital y su contraseña para obtener como resultado un documento firmado.
Las firmas digitales permiten verificar la autenticidad e integridad de un PDF. Para firmar un documento, necesitarás un certificado en formato
.pfx o .p12, junto con la contraseña asociada al certificado.Parte 1: Configuración de una Aplicación Laravel
Primero configuraremos una aplicación Laravel que se encargará de leer el PDF, cargar el certificado y realizar la solicitud a la API de PDF Signify. Si ya tienes un proyecto Laravel creado, puedes saltarte esta parte.
Crea un nuevo proyecto Laravel usando Composer:
Copy
composer create-project --prefer-dist laravel/laravel pdf-signing-appPaso 1: Configurar el entorno de Laravel
Copy
cd pdf-signing-app
cp .env.example .env
php artisan key:generatePaso 2: Configurar la base de datos
Abre el archivo
.env y configura la conexión a tu base de datos. En este ejemplo usamos MySQL.Copy
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=nombre_de_tu_base_de_datos
DB_USERNAME=nombre_de_usuario_de_tu_base_de_datos
DB_PASSWORD=contraseña_de_tu_base_de_datosPaso 3: Ejecutar las migraciones
Copy
php artisan migrateParte 2: Configuración de PDF Signify
Ahora configuraremos PDF Signify para poder firmar los documentos. Desde el panel podrás crear proyectos, gestionar límites de uso y generar las credenciales necesarias para autenticar las solicitudes.
Paso 1: Crear una cuenta o iniciar sesión
Entra en pdfsignify.com y crea una cuenta. También puedes iniciar sesión si ya tienes una.

Puedes registrarte manualmente o usando una cuenta de Google o Microsoft.

Paso 2: Verificar el correo electrónico
Después del registro, revisa tu bandeja de entrada y confirma tu correo electrónico usando el enlace de verificación.


Si no recibes el correo, revisa la carpeta de spam o solicita un nuevo email de verificación.

Paso 3: Crear un proyecto
Una vez dentro del panel, crea un nuevo proyecto. Esto te permitirá generar claves API, controlar el uso y gestionar las firmas digitales.

Selecciona un plan y asigna un nombre al proyecto.

Paso 4: Crear credenciales API
Ve a la sección API Credentials y pulsa en Create Credentials. Se generará una clave de acceso y una clave secreta.

Guarda la Secret Key en un lugar seguro. Normalmente solo se muestra una vez, así que si la pierdes tendrás que regenerar las credenciales.

Parte 3: Preparar el PDF y el Certificado
Para este ejemplo usaremos un PDF ya creado. Guarda el PDF dentro de
storage/app/ con el nombre filepdf.pdf.Copy
laravel-project/
│
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── routes/
├── storage/
│ └── app/
│ └── filepdf.pdf
├── tests/
└── artisanDespués, guarda tu certificado digital en el mismo directorio. Puede estar en formato
.pfx o .p12. En este ejemplo usaremos certificate.pfx.Copy
laravel-project/
│
├── storage/
│ └── app/
│ ├── filepdf.pdf
│ └── certificate.pfx
└── artisanParte 4: Programar la Firma en Laravel / PHP
Paso 1: Crear la ruta
Abre el archivo
routes/web.php y añade una ruta para firmar el PDF.Copy
use App\Http\Controllers\PDFSignController;
Route::get('/sign-pdf', [PDFSignController::class, 'signPDF'])->name('sign.pdf');Paso 2: Crear o editar el controlador
Crea el controlador
PDFSignController:Copy
php artisan make:controller PDFSignControllerPuedes empezar con una función básica para comprobar que la ruta funciona:
Copy
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Storage;
use CURLFile;
class PDFSignController extends Controller
{
public function signPDF()
{
dd("Sign pdf");
}
}Paso 3: Leer el PDF, el certificado y la contraseña
Copy
$certPath = Storage::path('certificate.pfx');
$pdfPath = Storage::path('filepdf.pdf');
$password = "YOUR_CERTIFICATE_PASSWORD";Paso 4: Crear los datos del formulario
La API espera una petición
multipart/form-data con el certificado, la contraseña y el PDF.Copy
$postFields = [
'certificate' => new CURLFile($certPath, 'application/x-pkcs12', 'certificate.pfx'),
'certificatePassword' => $password,
'pdf' => new CURLFile($pdfPath, 'application/pdf', 'filepdf.pdf')
];Paso 5: Enviar la solicitud con cURL
Copy
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.pdfsignify.com/api/v1/sign-pdf');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [
'Content-Type: multipart/form-data',
'AccessKey: ' . "MY_ACCESS_KEY",
'SecretKey: ' . "MY_SECRET_KEY"
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statusCode != 200) {
return response()->json(['response' => $response], 500);
}
return response($response, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="signed_filepdf.pdf"');Reemplaza
MY_ACCESS_KEY, MY_SECRET_KEY y YOUR_CERTIFICATE_PASSWORD por tus valores reales.Paso 6: Controlador completo
Copy
<?php
namespace App\Http\Controllers;
use CURLFile;
use Illuminate\Support\Facades\Storage;
class PDFSignController extends Controller
{
public function signPDF()
{
$certPath = Storage::path('certificate.pfx');
$pdfPath = Storage::path('filepdf.pdf');
$password = "YOUR_CERTIFICATE_PASSWORD";
$postFields = [
'certificate' => new CURLFile($certPath, 'application/x-pkcs12', 'certificate.pfx'),
'certificatePassword' => $password,
'pdf' => new CURLFile($pdfPath, 'application/pdf', 'filepdf.pdf')
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.pdfsignify.com/api/v1/sign-pdf');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [
'Content-Type: multipart/form-data',
'AccessKey: ' . "MY_ACCESS_KEY",
'SecretKey: ' . "MY_SECRET_KEY"
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statusCode != 200) {
return response()->json(['response' => $response], 500);
}
return response($response, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="signed_filepdf.pdf"');
}
}Paso 7: Probar la ruta
Copy
php artisan serveAbre
http://localhost:8000/sign-pdf. Si todo está correcto, se descargará el PDF firmado.Parte 5: Verificar el Resultado
Errores comunes
- El PDF no está guardado con el nombre correcto.
- El certificado no está en la ruta correcta o tiene una extensión incorrecta.
- La contraseña del certificado no es válida.
- Las credenciales de la API no son correctas.
Respuesta correcta
Si la solicitud funciona, recibirás un PDF firmado. Ten en cuenta que algunos navegadores no muestran bien la información de firma digital. Es mejor descargarlo y abrirlo con Adobe Reader u otro visor PDF completo.
Por defecto, la firma visible puede aparecer en la esquina inferior izquierda del documento.

Parte 6: Personalizar la Firma
PDF Signify permite personalizar la apariencia de la firma: posición, tamaño, página, mensaje, fecha, zona horaria, imagen y otros parámetros.
Paso 1: Imagen de fondo de la firma
Para usar una imagen de fondo puedes enviar el parámetro
signatureBackgroundImage.Copy
$signatureBackgroundImage = Storage::path('backgroundImage.png');
$postFields = [
'certificate' => new CURLFile($certPath, 'application/x-pkcs12', 'certificate.pfx'),
'certificatePassword' => $password,
'pdf' => new CURLFile($pdfPath, 'application/pdf', 'filepdf.pdf'),
'signatureBackgroundImage' => new CURLFile($signatureBackgroundImage, 'image/png', 'logo.png'),
];Paso 2: Imagen cerca de la firma
También puedes usar el parámetro
signatureImage para colocar una imagen junto a la firma.Copy
$signatureImage = Storage::path('signatureImage.png');
$postFields = [
'certificate' => new CURLFile($certPath, 'application/x-pkcs12', 'certificate.pfx'),
'certificatePassword' => $password,
'pdf' => new CURLFile($pdfPath, 'application/pdf', 'filepdf.pdf'),
'signatureImage' => new CURLFile($signatureImage, 'image/png', 'logo.png'),
];Paso 3: Parámetros de apariencia
Algunos parámetros útiles son
signatureXPosition, signatureYPosition, signatureWidth, signatureHeight, signatureMessage y signaturePageAppearance.Copy
$postFields['signaturePageAppearance'] = -1;
$postFields['timezone'] = 'UTC';
$postFields['signatureMessage'] = 'Digitally signed by the user';
$postFields['signatureDateLabel'] = '';
$postFields['signatureDateFormat'] = 'Y-m-d H:i:s';
$postFields['signatureHeight'] = 100;
$postFields['signatureWidth'] = 150;
$postFields['signatureYPosition'] = 100;
$postFields['signatureXPosition'] = 180;Parte 7: Metadatos del PDF
Al firmar un PDF también puedes modificar sus metadatos, como título, autor, asunto, palabras clave, creador, productor y fechas de creación o modificación.
Copy
$postFields['pdfMetadataAuthor'] = 'AUTHOR';
$postFields['pdfMetadataKeywords'] = 'keywords,keyword2';
$postFields['pdfMetadataTitle'] = 'MY DOCUMENT';
$postFields['pdfMetadataSubject'] = 'EXAMPLE DOCUMENT';
$postFields['pdfMetadataCreator'] = 'PDFSIGNIFY';
$postFields['pdfMetadataProducer'] = 'PDFSIGNIFY';
$currentDateTime = (new \DateTime())->format('Y-m-d H:i:s');
$postFields['pdfMetadataCreationDate'] = $currentDateTime;
$postFields['pdfMetadataModificationDate'] = $currentDateTime;También puedes usar el endpoint
/api/v1/set-pdf-metadata si solo quieres cambiar los metadatos sin firmar el documento.Parte 8: Ejemplo Completo con Personalización
Copy
<?php
namespace App\Http\Controllers;
use CURLFile;
use Illuminate\Support\Facades\Storage;
class PDFSignController extends Controller
{
public function signPDF()
{
$certPath = Storage::path('certificate.pfx');
$pdfPath = Storage::path('filepdf.pdf');
$password = "password";
$signatureBackgroundImage = Storage::path('backgroundImage.png');
$postFields = [
'certificate' => new CURLFile($certPath, 'application/x-pkcs12', 'certificate.pfx'),
'certificatePassword' => $password,
'pdf' => new CURLFile($pdfPath, 'application/pdf', 'filepdf.pdf'),
'signatureBackgroundImage' => new CURLFile($signatureBackgroundImage, 'image/png', 'logo.png'),
];
$postFields['signaturePageAppearance'] = -1;
$postFields['timezone'] = 'UTC';
$postFields['signatureMessage'] = 'Digitally signed by the user';
$postFields['signatureDateLabel'] = '';
$postFields['signatureDateFormat'] = 'Y-m-d H:i:s';
$postFields['signatureHeight'] = 100;
$postFields['signatureWidth'] = 150;
$postFields['signatureYPosition'] = 100;
$postFields['signatureXPosition'] = 180;
$postFields['pdfMetadataAuthor'] = 'AUTHOR';
$postFields['pdfMetadataKeywords'] = 'keywords,keyword2';
$postFields['pdfMetadataTitle'] = 'MY DOCUMENT';
$postFields['pdfMetadataSubject'] = 'EXAMPLE DOCUMENT';
$postFields['pdfMetadataCreator'] = 'PDFSIGNIFY';
$postFields['pdfMetadataProducer'] = 'PDFSIGNIFY';
$currentDateTime = (new \DateTime())->format('Y-m-d H:i:s');
$postFields['pdfMetadataCreationDate'] = $currentDateTime;
$postFields['pdfMetadataModificationDate'] = $currentDateTime;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.pdfsignify.com/api/v1/sign-pdf');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [
'Content-Type: multipart/form-data',
'AccessKey: ' . "MY_ACCESS_KEY",
'SecretKey: ' . "MY_SECRET_KEY"
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statusCode != 200) {
return response()->json(['response' => $response], 500);
}
return response($response, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="signed_filepdf.pdf"');
}
}Parte 9: Verificar la Contraseña del Certificado
PDF Signify también permite comprobar si un certificado es válido y si la contraseña coincide usando el endpoint
/api/v1/check-certificate-password.Copy
Route::get('/check-certificate', [PDFSignController::class, 'checkCertificate'])->name('check.certificate');Copy
public function checkCertificate()
{
$certPath = Storage::path('certificate.pfx');
$password = "password";
$postFields = [
'certificate' => new CURLFile($certPath, 'application/x-pkcs12', 'certificate.pfx'),
'certificatePassword' => $password,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.pdfsignify.com/api/v1/check-certificate-password');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [
'Content-Type: multipart/form-data',
'AccessKey: ' . "MY_ACCESS_KEY",
'SecretKey: ' . "MY_SECRET_KEY"
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
curl_close($ch);
return response()->json(['response' => $response]);
}Una respuesta correcta puede ser similar a esta:
Copy
{
"success": true,
"message": "¡La contraseña del certificado es correcta!",
"errors": [],
"data": {},
"auth": true
}Si la contraseña no es correcta, la respuesta puede ser similar a esta:
Copy
{
"success": false,
"message": "¡La contraseña del certificado no es correcta!",
"errors": [
"certificatePassword"
],
"data": {},
"auth": true
}Parte 10: Conclusiones
En este tutorial hemos visto cómo firmar PDFs usando Laravel, PHP o Symfony junto con PDF Signify. También hemos visto cómo personalizar la firma, modificar metadatos del PDF y validar la contraseña de un certificado digital.
Puedes consultar más información en estos recursos:
- Tutorial y código en GitHub: https://github.com/arnaullfe/pdfsignify-php-laravel-symfony-example
- Documentación de la API: https://api.pdfsignify.com/api/documentation#/
- Ejemplos: https://pdfsignify.com/#examples
- Sitio web: https://pdfsignify.com/