Skip to content

Commit

Permalink
Merge pull request #13 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.0.5
  • Loading branch information
josemmo authored Dec 23, 2017
2 parents df2a625 + 78c6959 commit 65c3023
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 17 deletions.
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,17 +295,34 @@ Por defecto, si se establece una forma de pago y no se indica la fecha de vencim
### Firmado de facturas
Aunque es posible exportar las facturas sin firmarlas, es un paso obligatorio para prácticamente cualquier trámite relacionado con la Administración Pública.
Para firmar facturas se necesita un certificado electrónico (generalmente expedido por la FNMT) del que extraer su clave pública y su clave privada.
Los siguientes comandos permiten extraer el certificado (clave pública) y la clave privada de un archivo PFX:

Por defecto, al firmar una factura se utilizan la fecha y hora actuales como sello de tiempo. Si se quiere indicar otro valor, se debe utilizar el siguiente método:

```php
$fac->setSignTime("2017-01-01T12:34:56+02:00");
```
openssl pkcs12 -in certificado_de_entrada.pfx -clcerts -nokeys -out clave_publica.pem
openssl pkcs12 -in certificado_de_entrada.pfx -nocerts -out clave_privada.pem

Llegados a este punto hay dos formas de facilitar estos datos a Facturae-PHP:

#### Firmado con clave pública y privada X.509
Si se tienen las clave pública y privada en archivos independientes se debe utilizar este método con los siguientes argumentos:

```php
$fac->sign("clave_publica.pem", "clave_privada.pem", "passphrase");
```

Por defecto, al firmar una factura se utilizan la fecha y hora actuales como sello de tiempo. Si se quiere indicar otro valor, se debe utilizar el siguiente método:
> Los siguientes comandos permiten extraer el certificado (clave pública) y la clave privada de un archivo PFX:
>
> ```
> openssl pkcs12 -in certificado_de_entrada.pfx -clcerts -nokeys -out clave_publica.pem
> openssl pkcs12 -in certificado_de_entrada.pfx -nocerts -out clave_privada.pem
> ```
#### Firmado con PKCS#12
Desde la versión 1.0.5 de Facturae-PHP ya es posible cargar un banco de certificados desde un archivo `.pfx` o `.p12` sin necesidad de convertirlo previamente a X.509:
```php
$fac->setSignTime("2017-01-01T12:34:56+02:00");
$fac->sign("certificado.pfx", NULL, "passphrase");
```
### Otros métodos
Expand Down Expand Up @@ -354,6 +371,7 @@ $totales = $fac->getTotals();
|Constante|Descripción|
|--------:|:----------|
|`Facturae::SCHEMA_3_2_1`|Formato de Facturae 3.2.1|
|`Facturae::SCHEMA_3_2_2`|Formato de Facturae 3.2.2|
|`Facturae::SIGN_POLICY_3_1`|Formato de firma 3.1|
|||
|`Facturae::PAYMENT_CASH`|Pago al contado|
Expand Down
72 changes: 63 additions & 9 deletions src/Facturae.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* This file contains everything you need to create invoices.
*
* @package josemmo\Facturae
* @version 1.0.4
* @version 1.0.5
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @author josemmo
*/
Expand All @@ -24,6 +24,7 @@ class Facturae {

/* CONSTANTS */
const SCHEMA_3_2_1 = "3.2.1";
const SCHEMA_3_2_2 = "3.2.2";
const SIGN_POLICY_3_1 = array(
"name" => "Política de Firma FacturaE v3.1",
"url" => "http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf",
Expand Down Expand Up @@ -101,10 +102,14 @@ public function __construct($schemaVersion=self::SCHEMA_3_2_1) {
* This method is used for generating random IDs required when signing the
* document.
*
* @return int Random number
* @return int Random number
*/
private function random() {
return rand(100000, 999999);
if (function_exists('random_int')) {
return random_int(0x10000000, 0x7FFFFFFF);
} else {
return rand(100000, 999999);
}
}


Expand Down Expand Up @@ -348,17 +353,57 @@ public function setSignTime($time) {


/**
* Sign
* Load a PKCS#12 Certificate Store
*
* @param string $pkcs12File The certificate store file name
* @param string $pkcs12Pass Encryption password for unlocking the PKCS#12 file
* @return bool Success
*/
private function loadPkcs12($pkcs12File, $pkcs12Pass="") {
if (!is_file($pkcs12File)) return false;

// Extract public and private keys from store
if (openssl_pkcs12_read(file_get_contents($pkcs12File), $certs, $pkcs12Pass)) {
$this->publicKey = openssl_x509_read($certs['cert']);
$this->privateKey = openssl_pkey_get_private($certs['pkey']);
}

return (!empty($this->publicKey) && !empty($this->privateKey));
}


/**
* Load a X.509 certificate and PEM encoded private key
*
* @param string $publicPath Path to public key PEM file
* @param string $privatePath Path to private key PEM file
* @param string $passphrase Private key passphrase
* @return bool Success
*/
private function loadX509($publicPath, $privatePath, $passphrase="") {
if (is_file($publicPath) && is_file($privatePath)) {
$this->publicKey = openssl_x509_read(file_get_contents($publicPath));
$this->privateKey = openssl_pkey_get_private(
file_get_contents($privatePath),
$passphrase
);
}
return (!empty($this->publicKey) && !empty($this->privateKey));
}


/**
* Sign
*
* @param string $publicPath Path to public key PEM file or PKCS#12 certificate store
* @param string $privatePath Path to private key PEM file (should be NULL in case of PKCS#12)
* @param string $passphrase Private key passphrase
* @param array $policy Facturae sign policy
* @return bool Success
*/
public function sign($publicPath, $privatePath, $passphrase, $policy=self::SIGN_POLICY_3_1) {
$this->publicKey = openssl_x509_read(file_get_contents($publicPath));
$this->privateKey = openssl_pkey_get_private(
file_get_contents($privatePath), $passphrase);
public function sign($publicPath, $privatePath=NULL, $passphrase="", $policy=self::SIGN_POLICY_3_1) {
$this->publicKey = NULL;
$this->privateKey = NULL;
$this->signPolicy = $policy;

// Generate random IDs
Expand All @@ -370,6 +415,13 @@ public function sign($publicPath, $privatePath, $passphrase, $policy=self::SIGN_
$this->referenceID = $this->random();
$this->signatureSignedPropertiesID = $this->random();
$this->signatureObjectID = $this->random();

// Load public and private keys
if (empty($privatePath)) {
return $this->loadPkcs12($publicPath, $passphrase);
} else {
return $this->loadX509($publicPath, $privatePath, $passphrase);
}
}


Expand All @@ -380,7 +432,7 @@ public function sign($publicPath, $privatePath, $passphrase, $policy=self::SIGN_
*/
private function injectSignature($xml) {
// Make sure we have all we need to sign the document
if (is_null($this->publicKey) || is_null($this->privateKey)) return $xml;
if (empty($this->publicKey) || empty($this->privateKey)) return $xml;

// Normalize document
$xml = str_replace("\r", "", $xml);
Expand All @@ -400,6 +452,8 @@ private function injectSignature($xml) {
"OU=" . $certData['issuer']['OU'] . "," .
"O=" . $certData['issuer']['O'] . "," .
"C=" . $certData['issuer']['C'];

// Generate signed properties
$prop = '<etsi:SignedProperties Id="Signature' . $this->signatureID .
'-SignedProperties' . $this->signatureSignedPropertiesID . '">' .
'<etsi:SignedSignatureProperties><etsi:SigningTime>' .
Expand Down
25 changes: 22 additions & 3 deletions tests/FacturaeTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?php

use josemmo\Facturae\Facturae;
use josemmo\Facturae\FacturaeItem;
use josemmo\Facturae\FacturaeParty;
Expand All @@ -10,7 +9,12 @@ final class FacturaeTest extends TestCase {
const FILE_PATH = __DIR__ . "/salida.xsig";
const COOKIES_PATH = __DIR__ . "/cookies.txt";

public function testCreateInvoice() {

/**
* Test Create Invoice
* @param boolean $isPfx Whether to test with PFX signature or PEM files
*/
public function testCreateInvoice($isPfx=false) {
// Creamos la factura
$fac = new Facturae();

Expand Down Expand Up @@ -97,14 +101,29 @@ public function testCreateInvoice() {
$fac->addLegalLiteral("Y este, otro (se pueden añadir varios)");

// Ya solo queda firmar la factura ...
$fac->sign(__DIR__ . "/public.pem", __DIR__ . "/private.pem", "12345");
if ($isPfx) {
$fac->sign(__DIR__ . "/test.pfx", NULL, "12345");
} else {
$fac->sign(__DIR__ . "/public.pem", __DIR__ . "/private.pem", "12345");
}

// ... y exportarlo a un archivo
$res = $fac->export(self::FILE_PATH);
$this->assertEquals($res, true);
}


/**
* Test PFX
*/
public function testPfx() {
$this->testCreateInvoice(true);
}


/**
* Test Invoice Complies with Format
*/
public function testInvoiceCompliesWithFormat() {
// Prepare file to upload
if (function_exists('curl_file_create')) {
Expand Down
Binary file added tests/test.pfx
Binary file not shown.

0 comments on commit 65c3023

Please sign in to comment.