Nos hemos quedado aquí:
Código PHP:
$guardar_como = $tbai_alb.'.xsig';
require_once($_SERVER['DOCUMENT_ROOT'].'/xxx/ticketBAI/src/XmlTools.php' );
require_once($_SERVER['DOCUMENT_ROOT'].'/xxx/ticketBAI/src/firmador.php' );
$dir_cert = ($_SERVER['DOCUMENT_ROOT'].'/xxx/_certificandos'.'/xxx.pfx' );
$fac = new Firmador();
$xmlF = $fac -> firmar($dir_cert,$cal_cert, $xml, $hacienda_foral);
$directorio_ventas = $directorio_TBAI.$guardar_como;
file_put_contents($directorio_ventas, $xmlF);
Lo que hago es indicar donde guardarlo, en mi caso con el mismo nombre número de orden de emisión de la factura.
XmlTools.php:
Código PHP:
class XmlTools {
public function randomId() {
if (function_exists('random_int')) return random_int(0x10000000, 0x7FFFFFFF);
return rand(100000, 999999);
}
public function generateGUID($prefix='pfx')
{
$uuid = md5(uniqid(mt_rand(), true));
$guid = $prefix.substr($uuid, 0, 8)."-".
substr($uuid, 8, 4)."-".
substr($uuid, 12, 4)."-".
substr($uuid, 16, 4)."-".
substr($uuid, 20, 12);
return $guid;
}
}
</div>
Este es el importante:
firmador.php
Código PHP:
<?php
class Firmador{
const POLITICA_FIRMA_BIZ = array(
"name" => "Politica de firma TicketBAI 1.0",
"url" => "https://www.batuz.eus/fitxategiak/batuz/ticketbai/sinadura_elektronikoaren_zehaztapenak_especificaciones_de_la_firma_electronica_v1_0.pdf",
"digest" => "Quzn98x3PMbSHwbUzaj5f5KOpiH0u8bvmwbbbNkO9Es="
);
const POLITICA_FIRMA_GIP = array(
"name" => "Politica de firma TicketBAI 1.0",
"url" => "https://www.gipuzkoa.eus/ticketbai/sinadura",
"digest" => "dTtPpv4fWTcejeVx7+91ILruFX3HysbngBlllJm4i/E="
);
private $signTime = NULL;
private $signPolicy = NULL;
private $publicKey = NULL;
private $privateKey = NULL;
private $cerROOT = NULL;
private $cerINTERMEDIO = NULL;
private $tipoDoc = '01';
public function retC14DigestSha1($strcadena)
{
$strcadena = str_replace("\r", "", str_replace("\n", "", $strcadena));
$d1p = new DOMDocument('1.0', 'UTF-8');
$d1p->loadXML($strcadena);
$strdata = $d1p->C14N();
return base64_encode(hash('sha256' , $strdata, true ));
}
public function firmar($certificadop12, $clavecertificado, $xmlsinfirma, $hacienda_foral)
{
if (!$pfx = file_get_contents($certificadop12))
{
echo "Error: No se puede leer el fichero del certificado o no existe en la ruta especificada\n";
exit;
}
if (openssl_pkcs12_read($pfx, $key, $clavecertificado))
{
$this->publicKey = $key["cert"];
$this->privateKey = $key["pkey"];
$complem = openssl_pkey_get_details(openssl_pkey_get_private($this->privateKey));
$this->Modulus = base64_encode($complem['rsa']['n']);
$this->Exponent = base64_encode($complem['rsa']['e']);
}
else
{
echo "Error: No se puede leer el almacén de certificados o la clave no es la correcta.\n";
exit;
}
if ($hacienda_foral == "BIZ")
$this->signPolicy = self::POLITICA_FIRMA_BIZ;
if ($hacienda_foral == "GIP")
$this->signPolicy = self::POLITICA_FIRMA_GIP;
$tools = new XmlTools();
$this->signatureID = $tools->generateGUID('xmldsig-');
$this->signatureValue = $this->signatureID.'-sigvalue';
$this->XadesObjectId = "XadesObjectId-".$this->signatureID;
$this->KeyInfoId = "KeyInfoId-".$this->signatureID;
$this->Reference0Id = $this->signatureID.'-ref0';
$this->Reference1Id = "ReferenceKeyInfo";
$this->SignedProperties = $this->signatureID.'-signedprops';
$this->qualifyingProperties = "QualifyingProperties-".$this->signatureID;
$xml1 = $xmlsinfirma;
$xml1 = $this->insertaFirma($xml1);
return $xml1;
}
public function insertaFirma($xml){
if (is_null($this->publicKey) || is_null($this->privateKey))
return $xml;
$d = new DOMDocument('1.0', 'UTF-8');
$d->loadXML($xml);
$canonizadoreal = $d->C14N();
$xmlns = 'xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ' .
'xmlns:T="urn:ticketbai:emision" ' .
'xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"';
$xmnls_signeg = 'xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"';
$xmlns_keyinfo = 'xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"';
$xmnls_signedprops = 'xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"';
$signTime1 = date('Y-m-d\TH:i:s+01:00');
$certData = openssl_x509_parse($this->publicKey);
$certDigest = base64_encode(openssl_x509_fingerprint($this->publicKey, "sha256", true));
$certIssuer = array();
foreach ($certData['issuer'] as $item=>$value)
{
$certIssuer[] = $item . '=' . $value;
}
$certIssuer = implode(', ', array_reverse($certIssuer));
$prop = '<xades:SignedProperties Id="'.$this->SignedProperties.'">' .
'<xades:SignedSignatureProperties>'.
'<xades:SigningTime>'.$signTime1.'</xades:SigningTime>' .
'<xades:SigningCertificate>'.
'<xades:Cert>'.
'<xades:CertDigest>' .
'<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>'.
'<ds:DigestValue>'.$certDigest.'</ds:DigestValue>'.
'</xades:CertDigest>'.
'<xades:IssuerSerial>' .
'<ds:X509IssuerName>'.$certIssuer.'</ds:X509IssuerName>'.
'<ds:X509SerialNumber>'.$certData['serialNumber'].'</ds:X509SerialNumber>' .
'</xades:IssuerSerial>'.
'</xades:Cert>'.
'</xades:SigningCertificate>' .
'<xades:SignaturePolicyIdentifier>'.
'<xades:SignaturePolicyId>' .
'<xades:SigPolicyId>'.
'<xades:Identifier>'.$this->signPolicy['url'].'</xades:Identifier>'.
'<xades:Description>'.$this->signPolicy['name'].'</xades:Description>'.
'</xades:SigPolicyId>'.
'<xades:SigPolicyHash>' .
'<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />'.
'<ds:DigestValue>'.$this->signPolicy['digest'].'</ds:DigestValue>'.
'</xades:SigPolicyHash>'.
'<xades:SigPolicyQualifiers>'.
'<xades:SigPolicyQualifier>'.
'<xades:SPURI>'.$this->signPolicy['url'].'</xades:SPURI>'.
'</xades:SigPolicyQualifier>'.
'</xades:SigPolicyQualifiers>'.
'</xades:SignaturePolicyId>' .
'</xades:SignaturePolicyIdentifier>'.
'</xades:SignedSignatureProperties>'.
'<xades:SignedDataObjectProperties>'.
'<xades:DataObjectFormat ObjectReference="#'.$this->Reference0Id.'">'.
'<xades:ObjectIdentifier>' .
'<xades:Identifier Qualifier="OIDAsURN">urn:oid:1.2.840.10003.5.109.10</xades:Identifier>' .
'</xades:ObjectIdentifier>' .
'<xades:MimeType>text/xml</xades:MimeType>'.
'<xades:Encoding>UTF-8</xades:Encoding>'.
'</xades:DataObjectFormat>'.
'</xades:SignedDataObjectProperties>'.
'</xades:SignedProperties>';
// Prepare key info
$publicPEM = "";
openssl_x509_export($this->publicKey, $publicPEM);
$publicPEM = str_replace("-----BEGIN CERTIFICATE-----", "", $publicPEM);
$publicPEM = str_replace("-----END CERTIFICATE-----", "", $publicPEM);
$publicPEM = str_replace("\r", "", str_replace("\n", "", $publicPEM));
$kInfo = '<ds:KeyInfo Id="'.$this->KeyInfoId.'">' .
'<ds:X509Data>' .
'<ds:X509Certificate>'.$publicPEM.'</ds:X509Certificate>' .
'</ds:X509Data>' .
'<ds:KeyValue>'.
'<ds:RSAKeyValue>'.
'<ds:Modulus>'.$this->Modulus.'</ds:Modulus>'.
'<ds:Exponent>'.$this->Exponent.'</ds:Exponent>'.
'</ds:RSAKeyValue>'.
'</ds:KeyValue>'.
'</ds:KeyInfo>';
$keyinfo_para_hash1 = str_replace('<ds:KeyInfo', '<ds:KeyInfo ' . $xmlns_keyinfo, $kInfo);
$kInfoDigest = $this->retC14DigestSha1($keyinfo_para_hash1);
$aconop = str_replace('<xades:SignedProperties', '<xades:SignedProperties ' . $xmnls_signedprops, $prop);
$propDigest = $this->retC14DigestSha1($aconop);
$documentDigest = base64_encode(hash('sha256', $canonizadoreal, true));
// Prepare signed info
$sInfo = '<ds:SignedInfo>' .
'<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>' .
'<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>' .
'<ds:Reference URI="#'.$this->SignedProperties.'" Type="http://uri.etsi.org/01903#SignedProperties">' .
'<ds:Transforms>' .
'<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>' .
'</ds:Transforms>' .
'<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>' .
'<ds:DigestValue>'.$propDigest.'</ds:DigestValue>' .
'</ds:Reference>' .
'<ds:Reference Id="'.$this->Reference1Id.'" URI="#'.$this->KeyInfoId.'">' .
'<ds:Transforms>' .
'<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>' .
'</ds:Transforms>' .
'<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />' .
'<ds:DigestValue>'.$kInfoDigest.'</ds:DigestValue>' .
'</ds:Reference>' .
'<ds:Reference Id="'.$this->Reference0Id.'" URI=""
Type="http://www.w3.org/2000/09/xmldsig#Object">' .
'<ds:Transforms>' .
'<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>' .
'<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>' .
'</ds:Transforms>' .
'<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>' .
'<ds:DigestValue>'.$documentDigest.'</ds:DigestValue>' .
'</ds:Reference>' .
'</ds:SignedInfo>';
$signaturePayload = str_replace('<ds:SignedInfo', '<ds:SignedInfo ' . $xmnls_signeg, $sInfo);
$d1p = new DOMDocument('1.0', 'UTF-8');
$d1p->loadXML($signaturePayload);
$signaturePayload = $d1p->C14N();
$signatureResult = "";
$algo = "SHA256";
openssl_sign($signaturePayload, $signatureResult, $this->privateKey, $algo);
$signatureResult = base64_encode($signatureResult);
$sig = '<ds:Signature Id="'.$this->signatureID.'" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">'.
$sInfo.'<ds:SignatureValue Id="'.$this->signatureValue.'">'.$signatureResult. '
</ds:SignatureValue>'.
$kInfo .
'<ds:Object Id="'.$this->XadesObjectId .'">'.
'<xades:QualifyingProperties Id="'.$this->qualifyingProperties.'"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Target="#'.$this->signatureID.'"
xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">'.
$prop .
'</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>';
$xml = str_replace('</T:TicketBai>', $sig . '</T:TicketBai>', $xml);
return $xml;
}
}
?>
Mucha atención al contenido de
firmador.php
Es posible que algo sobre pero no me atrevo a tocar nada ya que finciona bien.
Proximamente hablaremos del envío de los xml firmados a GIP y a BIZ, este último tiene su miga.
A vuestra disposición para comentar y aclarar lo que requiráis
Sería bueno poder firmar sólo con los .pem y sin clave de certificado pero quizás no se pueda.
Un saludo