@@ -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