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'
+ ])
+ );
+ }
+
}