diff --git a/src/Common/FacturaeSigner.php b/src/Common/FacturaeSigner.php index b279e31..ba512db 100644 --- a/src/Common/FacturaeSigner.php +++ b/src/Common/FacturaeSigner.php @@ -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; @@ -174,22 +153,6 @@ public function sign($xml) { // Build 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 = '' . '' . '' . date('c', $signingTime) . '' . @@ -200,7 +163,7 @@ public function sign($xml) { '' . XmlTools::getCertDigest($this->publicChain[0]) . '' . '' . '' . - '' . $certIssuer . '' . + '' . XmlTools::getCertDistinguishedName($certData['issuer']) . '' . '' . $certData['serialNumber'] . '' . '' . '' . diff --git a/src/Common/XmlTools.php b/src/Common/XmlTools.php index d8f46e3..90c0f90 100644 --- a/src/Common/XmlTools.php +++ b/src/Common/XmlTools.php @@ -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 @@ -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 diff --git a/src/Facturae.php b/src/Facturae.php index e3401cb..3186aad 100644 --- a/src/Facturae.php +++ b/src/Facturae.php @@ -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"; diff --git a/tests/XmlToolsTest.php b/tests/XmlToolsTest.php index 1235c00..9db3bce 100644 --- a/tests/XmlToolsTest.php +++ b/tests/XmlToolsTest.php @@ -45,4 +45,59 @@ public function testCanCanonicalizeXml() { $this->assertEquals('', $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' + ]) + ); + } + }