1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Selective\XmlDSig; |
4
|
|
|
|
5
|
|
|
use DOMDocument; |
6
|
|
|
use DOMElement; |
7
|
|
|
use DOMXPath; |
8
|
|
|
use OpenSSLCertificate; |
9
|
|
|
use Selective\XmlDSig\Exception\XmlSignerException; |
10
|
|
|
use UnexpectedValueException; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Sign XML Documents with Digital Signatures (XMLDSIG). |
14
|
|
|
*/ |
15
|
|
|
final class XmlSigner |
16
|
|
|
{ |
17
|
|
|
private string $referenceUri = ''; |
18
|
|
|
|
19
|
|
|
private XmlReader $xmlReader; |
20
|
|
|
|
21
|
|
|
private CryptoSignerInterface $cryptoSigner; |
22
|
|
|
|
23
|
3 |
|
public function __construct(CryptoSignerInterface $cryptoSigner) |
24
|
|
|
{ |
25
|
3 |
|
$this->xmlReader = new XmlReader(); |
26
|
3 |
|
$this->cryptoSigner = $cryptoSigner; |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Sign an XML file and save the signature in a new file. |
31
|
|
|
* This method does not save the public key within the XML file. |
32
|
|
|
* |
33
|
|
|
* @param string $data The XML content to sign |
34
|
|
|
* |
35
|
|
|
* @throws XmlSignerException |
36
|
|
|
* |
37
|
|
|
* @return string The signed XML content |
38
|
|
|
*/ |
39
|
3 |
|
public function signXml(string $data): string |
40
|
|
|
{ |
41
|
|
|
// Read the xml file content |
42
|
3 |
|
$xml = new DOMDocument(); |
43
|
|
|
|
44
|
|
|
// Whitespaces must be preserved |
45
|
3 |
|
$xml->preserveWhiteSpace = true; |
46
|
3 |
|
$xml->formatOutput = false; |
47
|
|
|
|
48
|
3 |
|
$xml->loadXML($data); |
49
|
|
|
|
50
|
|
|
// Canonicalize the content, exclusive and without comments |
51
|
3 |
|
if (!$xml->documentElement) { |
52
|
|
|
throw new XmlSignerException('Undefined document element'); |
53
|
|
|
} |
54
|
|
|
|
55
|
3 |
|
return $this->signDocument($xml); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Sign DOM document. |
60
|
|
|
* |
61
|
|
|
* @param DOMDocument $document The document |
62
|
|
|
* @param DOMElement|null $element The element of the document to sign |
63
|
|
|
* |
64
|
|
|
* @return string The signed XML as string |
65
|
|
|
*/ |
66
|
3 |
|
public function signDocument(DOMDocument $document, DOMElement $element = null): string |
67
|
|
|
{ |
68
|
3 |
|
$element = $element ?? $document->documentElement; |
69
|
|
|
|
70
|
3 |
|
if ($element === null) { |
71
|
|
|
throw new XmlSignerException('Invalid XML document element'); |
72
|
|
|
} |
73
|
|
|
|
74
|
3 |
|
$canonicalData = $element->C14N(true, false); |
75
|
|
|
|
76
|
|
|
// Calculate and encode digest value |
77
|
3 |
|
$digestValue = $this->cryptoSigner->computeDigest($canonicalData); |
78
|
|
|
|
79
|
3 |
|
$digestValue = base64_encode($digestValue); |
80
|
3 |
|
$this->appendSignature($document, $digestValue); |
81
|
|
|
|
82
|
3 |
|
$result = $document->saveXML(); |
83
|
|
|
|
84
|
3 |
|
if ($result === false) { |
85
|
|
|
throw new XmlSignerException('Signing failed. Invalid XML.'); |
86
|
|
|
} |
87
|
|
|
|
88
|
3 |
|
return $result; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Create the XML representation of the signature. |
93
|
|
|
* |
94
|
|
|
* @param DOMDocument $xml The xml document |
95
|
|
|
* @param string $digestValue The digest value |
96
|
|
|
* |
97
|
|
|
* @throws UnexpectedValueException |
98
|
|
|
* |
99
|
|
|
* @return void The DOM document |
100
|
|
|
*/ |
101
|
3 |
|
private function appendSignature(DOMDocument $xml, string $digestValue): void |
102
|
|
|
{ |
103
|
3 |
|
$signatureElement = $xml->createElement('Signature'); |
104
|
3 |
|
$signatureElement->setAttribute('xmlns', 'http://www.w3.org/2000/09/xmldsig#'); |
105
|
|
|
|
106
|
|
|
// Append the element to the XML document. |
107
|
|
|
// We insert the new element as root (child of the document) |
108
|
|
|
|
109
|
3 |
|
if (!$xml->documentElement) { |
110
|
|
|
throw new UnexpectedValueException('Undefined document element'); |
111
|
|
|
} |
112
|
|
|
|
113
|
3 |
|
$xml->documentElement->appendChild($signatureElement); |
114
|
|
|
|
115
|
3 |
|
$signedInfoElement = $xml->createElement('SignedInfo'); |
116
|
3 |
|
$signatureElement->appendChild($signedInfoElement); |
117
|
|
|
|
118
|
3 |
|
$canonicalizationMethodElement = $xml->createElement('CanonicalizationMethod'); |
119
|
3 |
|
$canonicalizationMethodElement->setAttribute('Algorithm', 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'); |
120
|
3 |
|
$signedInfoElement->appendChild($canonicalizationMethodElement); |
121
|
|
|
|
122
|
3 |
|
$signatureMethodElement = $xml->createElement('SignatureMethod'); |
123
|
3 |
|
$signatureMethodElement->setAttribute( |
124
|
|
|
'Algorithm', |
125
|
3 |
|
$this->cryptoSigner->getAlgorithm()->getSignatureAlgorithmUrl() |
126
|
|
|
); |
127
|
3 |
|
$signedInfoElement->appendChild($signatureMethodElement); |
128
|
|
|
|
129
|
3 |
|
$referenceElement = $xml->createElement('Reference'); |
130
|
3 |
|
$referenceElement->setAttribute('URI', $this->referenceUri); |
131
|
3 |
|
$signedInfoElement->appendChild($referenceElement); |
132
|
|
|
|
133
|
3 |
|
$transformsElement = $xml->createElement('Transforms'); |
134
|
3 |
|
$referenceElement->appendChild($transformsElement); |
135
|
|
|
|
136
|
|
|
// Enveloped: the <Signature> node is inside the XML we want to sign |
137
|
3 |
|
$transformElement = $xml->createElement('Transform'); |
138
|
3 |
|
$transformElement->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'); |
139
|
3 |
|
$transformsElement->appendChild($transformElement); |
140
|
|
|
|
141
|
3 |
|
$digestMethodElement = $xml->createElement('DigestMethod'); |
142
|
3 |
|
$digestMethodElement->setAttribute('Algorithm', $this->cryptoSigner->getAlgorithm()->getDigestAlgorithmUrl()); |
143
|
3 |
|
$referenceElement->appendChild($digestMethodElement); |
144
|
|
|
|
145
|
3 |
|
$digestValueElement = $xml->createElement('DigestValue', $digestValue); |
146
|
3 |
|
$referenceElement->appendChild($digestValueElement); |
147
|
|
|
|
148
|
3 |
|
$signatureValueElement = $xml->createElement('SignatureValue', ''); |
149
|
3 |
|
$signatureElement->appendChild($signatureValueElement); |
150
|
|
|
|
151
|
3 |
|
$keyInfoElement = $xml->createElement('KeyInfo'); |
152
|
3 |
|
$signatureElement->appendChild($keyInfoElement); |
153
|
|
|
|
154
|
3 |
|
$keyValueElement = $xml->createElement('KeyValue'); |
155
|
3 |
|
$keyInfoElement->appendChild($keyValueElement); |
156
|
|
|
|
157
|
3 |
|
$rsaKeyValueElement = $xml->createElement('RSAKeyValue'); |
158
|
3 |
|
$keyValueElement->appendChild($rsaKeyValueElement); |
159
|
|
|
|
160
|
3 |
|
$modulus = $this->cryptoSigner->getPrivateKeyStore()->getModulus(); |
161
|
3 |
|
if ($modulus) { |
162
|
2 |
|
$modulusElement = $xml->createElement('Modulus', $modulus); |
163
|
2 |
|
$rsaKeyValueElement->appendChild($modulusElement); |
164
|
|
|
} |
165
|
|
|
|
166
|
3 |
|
$publicExponent = $this->cryptoSigner->getPrivateKeyStore()->getPublicExponent(); |
167
|
3 |
|
if ($publicExponent) { |
168
|
2 |
|
$exponentElement = $xml->createElement('Exponent', $publicExponent); |
169
|
2 |
|
$rsaKeyValueElement->appendChild($exponentElement); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
// If certificates are loaded attach them to the KeyInfo element |
173
|
3 |
|
$certificates = $this->cryptoSigner->getPrivateKeyStore()->getCertificates(); |
174
|
3 |
|
if ($certificates) { |
|
|
|
|
175
|
|
|
$this->appendX509Certificates($xml, $keyInfoElement, $certificates); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
// http://www.soapclient.com/XMLCanon.html |
179
|
3 |
|
$c14nSignedInfo = $signedInfoElement->C14N(true, false); |
180
|
|
|
|
181
|
3 |
|
$signatureValue = $this->cryptoSigner->computeSignature($c14nSignedInfo); |
182
|
|
|
|
183
|
3 |
|
$xpath = new DOMXpath($xml); |
184
|
3 |
|
$signatureValueElement = $this->xmlReader->queryDomNode($xpath, '//SignatureValue', $signatureElement); |
185
|
3 |
|
$signatureValueElement->nodeValue = base64_encode($signatureValue); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Create and append an X509Data element containing certificates in base64 format. |
190
|
|
|
* |
191
|
|
|
* @param DOMDocument $xml |
192
|
|
|
* @param DOMElement $keyInfoElement |
193
|
|
|
* @param OpenSSLCertificate[] $certificates |
194
|
|
|
* |
195
|
|
|
* @return void |
196
|
|
|
*/ |
197
|
|
|
private function appendX509Certificates(DOMDocument $xml, DOMElement $keyInfoElement, array $certificates): void |
198
|
|
|
{ |
199
|
|
|
$x509DataElement = $xml->createElement('X509Data'); |
200
|
|
|
$keyInfoElement->appendChild($x509DataElement); |
201
|
|
|
|
202
|
|
|
$x509Reader = new X509Reader(); |
203
|
|
|
foreach ($certificates as $certificateId) { |
204
|
|
|
$certificate = $x509Reader->toRawBase64($certificateId); |
205
|
|
|
|
206
|
|
|
$x509CertificateElement = $xml->createElement('X509Certificate', $certificate); |
207
|
|
|
$x509DataElement->appendChild($x509CertificateElement); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Set reference URI. |
213
|
|
|
* |
214
|
|
|
* @param string $referenceUri The reference URI |
215
|
|
|
* |
216
|
|
|
* @return void |
217
|
|
|
*/ |
218
|
2 |
|
public function setReferenceUri(string $referenceUri): void |
219
|
|
|
{ |
220
|
2 |
|
$this->referenceUri = $referenceUri; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.