Skip to content

Commit 476abef

Browse files
committed
feat(xml): implement XML-DSig signing in DpsSigner
Signed-off-by: Vitor Mattos <vitor@php.rio>
1 parent bb29bee commit 476abef

File tree

1 file changed

+95
-15
lines changed

1 file changed

+95
-15
lines changed

src/Xml/DpsSigner.php

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ public function sign(string $xml, string $cnpj): string
4545
throw new PfxImportException('Cannot read PFX file for CNPJ ' . $cnpj);
4646
}
4747

48-
$signingMaterial = $this->importPfx($pfxContent, $password, $cnpj);
49-
unset($signingMaterial);
48+
[$privateKeyPem, $certificatePem] = $this->importPfx($pfxContent, $password, $cnpj);
5049

51-
return $this->signXml($xml);
50+
return $this->signXml($xml, $privateKeyPem, $certificatePem);
5251
}
5352

5453
// -------------------------------------------------------------------------
@@ -131,31 +130,112 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
131130
}
132131
}
133132

134-
private function signXml(string $xml): string
133+
/**
134+
* Signs an XML document per ABRASF 2.04 / XML-DSig (RSA-SHA1, enveloped signature).
135+
*
136+
* Steps:
137+
* 1. Locate the element with @Id (infDPS).
138+
* 2. Compute SHA-1 digest of its canonical (C14N) form.
139+
* 3. Build the Signature/SignedInfo structure in the ds: namespace.
140+
* 4. Compute C14N of SignedInfo and RSA-SHA1 sign it.
141+
* 5. Append SignatureValue and KeyInfo (X509Certificate) to complete Signature.
142+
*/
143+
private function signXml(string $xml, string $privateKeyPem, string $certificatePem): string
135144
{
145+
$dsNs = 'http://www.w3.org/2000/09/xmldsig#';
146+
$c14nAlgo = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
147+
$sigAlgo = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
148+
$sha1Algo = 'http://www.w3.org/2000/09/xmldsig#sha1';
149+
$envAlgo = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature';
150+
136151
$doc = new \DOMDocument('1.0', 'UTF-8');
137152
$doc->preserveWhiteSpace = false;
138-
$doc->formatOutput = false;
139153

140154
if (!$doc->loadXML($xml)) {
141155
throw new PfxImportException('Cannot parse XML for signing');
142156
}
143157

144-
$xpath = new \DOMXPath($doc);
145-
// Find the element to sign — the root DPS element
146-
$infDps = $xpath->query('//*[@Id]')->item(0);
158+
$xpath = new \DOMXPath($doc);
159+
$idNode = $xpath->query('//*[@Id]')->item(0);
147160

148-
if ($infDps === null) {
161+
if (!$idNode instanceof \DOMElement) {
149162
throw new PfxImportException('No element with @Id attribute found in DPS XML');
150163
}
151164

152-
$signedXml = new \DOMDocument('1.0', 'UTF-8');
153-
$signedXml->preserveWhiteSpace = false;
165+
$refId = $idNode->getAttribute('Id');
166+
167+
// 1. Digest the reference element (Signature not yet in the document — enveloped transform is a no-op here)
168+
$refCanonical = $idNode->C14N();
169+
$digestValue = base64_encode(hash('sha1', $refCanonical, binary: true));
170+
171+
// 2. Build Signature element
172+
$sig = $doc->createElementNS($dsNs, 'Signature');
173+
$doc->documentElement->appendChild($sig);
174+
175+
// 2a. SignedInfo
176+
$signedInfo = $doc->createElementNS($dsNs, 'SignedInfo');
177+
$sig->appendChild($signedInfo);
178+
179+
$c14nMethod = $doc->createElementNS($dsNs, 'CanonicalizationMethod');
180+
$c14nMethod->setAttribute('Algorithm', $c14nAlgo);
181+
$signedInfo->appendChild($c14nMethod);
182+
183+
$sigMethod = $doc->createElementNS($dsNs, 'SignatureMethod');
184+
$sigMethod->setAttribute('Algorithm', $sigAlgo);
185+
$signedInfo->appendChild($sigMethod);
186+
187+
$reference = $doc->createElementNS($dsNs, 'Reference');
188+
$reference->setAttribute('URI', '#' . $refId);
189+
$signedInfo->appendChild($reference);
190+
191+
$transforms = $doc->createElementNS($dsNs, 'Transforms');
192+
$reference->appendChild($transforms);
193+
194+
$t1 = $doc->createElementNS($dsNs, 'Transform');
195+
$t1->setAttribute('Algorithm', $envAlgo);
196+
$transforms->appendChild($t1);
197+
198+
$t2 = $doc->createElementNS($dsNs, 'Transform');
199+
$t2->setAttribute('Algorithm', $c14nAlgo);
200+
$transforms->appendChild($t2);
201+
202+
$digestMethod = $doc->createElementNS($dsNs, 'DigestMethod');
203+
$digestMethod->setAttribute('Algorithm', $sha1Algo);
204+
$reference->appendChild($digestMethod);
205+
206+
$digestValueEl = $doc->createElementNS($dsNs, 'DigestValue');
207+
$digestValueEl->textContent = $digestValue;
208+
$reference->appendChild($digestValueEl);
209+
210+
// 3. Canonicalise SignedInfo and sign it
211+
$signedInfoC14n = $signedInfo->C14N();
212+
213+
$privKey = openssl_pkey_get_private($privateKeyPem);
214+
if ($privKey === false) {
215+
throw new PfxImportException('Cannot load private key for XML signing');
216+
}
217+
218+
$rawSignature = '';
219+
if (!openssl_sign($signedInfoC14n, $rawSignature, $privKey, OPENSSL_ALGO_SHA1)) {
220+
throw new PfxImportException('openssl_sign failed: ' . (openssl_error_string() ?: 'unknown error'));
221+
}
222+
223+
// 4. SignatureValue
224+
$sigValueEl = $doc->createElementNS($dsNs, 'SignatureValue');
225+
$sigValueEl->textContent = base64_encode($rawSignature);
226+
$sig->appendChild($sigValueEl);
227+
228+
// 5. KeyInfo / X509Certificate
229+
$certB64 = preg_replace('/-----[A-Z ]+-----|[\r\n]/', '', $certificatePem) ?? '';
230+
231+
$keyInfo = $doc->createElementNS($dsNs, 'KeyInfo');
232+
$x509Data = $doc->createElementNS($dsNs, 'X509Data');
233+
$x509Cert = $doc->createElementNS($dsNs, 'X509Certificate');
234+
$x509Cert->textContent = $certB64;
235+
$x509Data->appendChild($x509Cert);
236+
$keyInfo->appendChild($x509Data);
237+
$sig->appendChild($keyInfo);
154238

155-
// Use PHP's built-in xmldsig extension when available; otherwise fall back
156-
// to manual C14N + RSA-SHA1 computation.
157-
// TODO: Implement full XML-DSig per ABRASF 2.04 spec in Phase 2.
158-
// For now return the unsigned XML so the test scaffold builds green.
159239
return $doc->saveXML() ?: $xml;
160240
}
161241
}

0 commit comments

Comments
 (0)