Skip to content

Commit

Permalink
Merge pull request #161 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.8.1
  • Loading branch information
josemmo authored Jun 16, 2024
2 parents 102edbb + f0026a0 commit c7f01f5
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 39 deletions.
39 changes: 1 addition & 38 deletions src/Common/FacturaeSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,6 @@ final class FacturaeSigner {
const SIGN_POLICY_NAME = 'Política de Firma FacturaE v3.1';
const SIGN_POLICY_URL = 'http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf';
const SIGN_POLICY_DIGEST = 'Ohixl6upD6av8N7pEvDABhEL6hM=';
const ALLOWED_OID_TYPES = [
// Mandatory fields in https://datatracker.ietf.org/doc/html/rfc4514#section-3
'CN' => 'CN',
'L' => 'L',
'ST' => 'ST',
'O' => 'O',
'OU' => 'OU',
'C' => 'C',
'STREET' => 'STREET',
'DC' => 'DC',
'UID' => 'UID',

// Other fields with well-known names
'GN' => 'GN',
'SN' => 'SN',

// Other fields with compatibility issues
'organizationIdentifier' => 'OID.2.5.4.97',
'serialNumber' => 'OID.2.5.4.5',
'title' => 'OID.2.5.4.12',
];

use KeyPairReaderTrait;

Expand Down Expand Up @@ -174,22 +153,6 @@ public function sign($xml) {
// Build <xades:SignedProperties /> element
$signingTime = ($this->signingTime === null) ? time() : $this->signingTime;
$certData = openssl_x509_parse($this->publicChain[0]);
$certIssuer = [];
foreach ($certData['issuer'] as $rawType=>$rawValues) {
$values = is_array($rawValues) ? $rawValues : [$rawValues];
foreach ($values as $value) {
if ($rawType === "UNDEF" && preg_match('/^VAT[A-Z]{2}-/', $value) === 1) {
$type = "OID.2.5.4.97"; // Fix for OpenSSL <3.0.0
} else {
if (!array_key_exists($rawType, self::ALLOWED_OID_TYPES)) {
continue; // Skip unknown OID types
}
$type = self::ALLOWED_OID_TYPES[$rawType];
}
$certIssuer[] = "$type=$value";
}
}
$certIssuer = implode(', ', array_reverse($certIssuer));
$xadesSignedProperties = '<xades:SignedProperties Id="'. $this->signatureSignedPropertiesId . '">' .
'<xades:SignedSignatureProperties>' .
'<xades:SigningTime>' . date('c', $signingTime) . '</xades:SigningTime>' .
Expand All @@ -200,7 +163,7 @@ public function sign($xml) {
'<ds:DigestValue>' . XmlTools::getCertDigest($this->publicChain[0]) . '</ds:DigestValue>' .
'</xades:CertDigest>' .
'<xades:IssuerSerial>' .
'<ds:X509IssuerName>' . $certIssuer . '</ds:X509IssuerName>' .
'<ds:X509IssuerName>' . XmlTools::getCertDistinguishedName($certData['issuer']) . '</ds:X509IssuerName>' .
'<ds:X509SerialNumber>' . $certData['serialNumber'] . '</ds:X509SerialNumber>' .
'</xades:IssuerSerial>' .
'</xades:Cert>' .
Expand Down
54 changes: 54 additions & 0 deletions src/Common/XmlTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@
namespace josemmo\Facturae\Common;

class XmlTools {
const ALLOWED_OID_TYPES = [
// Mandatory fields in https://datatracker.ietf.org/doc/html/rfc4514#section-3
'CN' => 'CN',
'L' => 'L',
'ST' => 'ST',
'O' => 'O',
'OU' => 'OU',
'C' => 'C',
'STREET' => 'STREET',
'DC' => 'DC',
'UID' => 'UID',

// Other fields with well-known names
'GN' => 'GN',
'SN' => 'SN',

// Other fields with compatibility issues
'organizationIdentifier' => 'OID.2.5.4.97',
'serialNumber' => 'OID.2.5.4.5',
'title' => 'OID.2.5.4.12',
];

/**
* Escape XML value
Expand Down Expand Up @@ -164,6 +185,39 @@ public static function getCertDigest($publicKey, $pretty=false) {
}


/**
* Get certificate distinguished name
* @param array $data Certificate issuer or subject name data
* @return string Distinguished name
*/
public static function getCertDistinguishedName($data) {
$name = [];
foreach ($data as $rawType=>$rawValues) {
$values = is_array($rawValues) ? $rawValues : [$rawValues];
foreach ($values as $value) {
// Default case: allowed OID type
if (array_key_exists($rawType, self::ALLOWED_OID_TYPES)) {
$type = self::ALLOWED_OID_TYPES[$rawType];
$name[] = "$type=$value";
continue;
}

// Fix for undefined properties in OpenSSL <3.0.0
if ($rawType === "UNDEF") {
$decodedValue = (substr($value, 0, 1) === '#') ? hex2bin(substr($value, 5)) : $value;
if (preg_match('/^VAT[A-Z]{2}-/', $decodedValue) === 1) {
$name[] = "OID.2.5.4.97=$value";
}
}

// Unknown OID type, ignore
}
}
$name = implode(', ', array_reverse($name));
return $name;
}


/**
* Get signature in SHA-512
* @param string $payload Data to sign
Expand Down
2 changes: 1 addition & 1 deletion src/Facturae.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Class for creating electronic invoices that comply with the Spanish FacturaE format.
*/
class Facturae {
const VERSION = "1.8.0";
const VERSION = "1.8.1";
const USER_AGENT = "FacturaePHP/" . self::VERSION;

const SCHEMA_3_2 = "3.2";
Expand Down
55 changes: 55 additions & 0 deletions tests/XmlToolsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,59 @@ public function testCanCanonicalizeXml() {
$this->assertEquals('<abc:hello><xyz:world></xyz:world><earth></earth><everyone></everyone></abc:hello>', $c14n);
}


public function testCanGenerateDistinguishedNames() {
$this->assertEquals(
'CN=EIDAS CERTIFICADO PRUEBAS - 99999999R, SN=EIDAS CERTIFICADO, GN=PRUEBAS, OID.2.5.4.5=IDCES-99999999R, C=ES',
XmlTools::getCertDistinguishedName([
'C' => 'ES',
'serialNumber' => 'IDCES-99999999R',
'GN' => 'PRUEBAS',
'SN' => 'EIDAS CERTIFICADO',
'CN' => 'EIDAS CERTIFICADO PRUEBAS - 99999999R'
])
);
$this->assertEquals(
'OID.2.5.4.97=VATFR-12345678901, CN=A Common Name, OU=Field, OU=Repeated, C=FR',
XmlTools::getCertDistinguishedName([
'C' => 'FR',
'OU' => ['Repeated', 'Field'],
'CN' => 'A Common Name',
'ignoreMe' => 'This should not be here',
'organizationIdentifier' => 'VATFR-12345678901',
])
);
$this->assertEquals(
'OID.2.5.4.97=VATES-A11223344, CN=ACME ROOT, OU=ACME-CA, O=ACME Inc., L=Barcelona, C=ES',
XmlTools::getCertDistinguishedName([
'C' => 'ES',
'L' => 'Barcelona',
'O' => 'ACME Inc.',
'OU' => 'ACME-CA',
'CN' => 'ACME ROOT',
'UNDEF' => 'VATES-A11223344'
])
);
$this->assertEquals(
'OID.2.5.4.97=#0c0f56415445532d413030303030303030, CN=Common Name (UTF-8), OU=Unit, O=Organization, C=ES',
XmlTools::getCertDistinguishedName([
'C' => 'ES',
'O' => 'Organization',
'OU' => 'Unit',
'CN' => 'Common Name (UTF-8)',
'UNDEF' => '#0c0f56415445532d413030303030303030'
])
);
$this->assertEquals(
'OID.2.5.4.97=#130f56415445532d413636373231343939, CN=Common Name (printable), OU=Unit, O=Organization, C=ES',
XmlTools::getCertDistinguishedName([
'C' => 'ES',
'O' => 'Organization',
'OU' => 'Unit',
'CN' => 'Common Name (printable)',
'UNDEF' => '#130f56415445532d413636373231343939'
])
);
}

}

0 comments on commit c7f01f5

Please sign in to comment.