Passed
Push — master ( 18598b...5b9510 )
by Daniel
11:40
created

XmlSigner::appendSignature()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 85
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 5.0014

Importance

Changes 0
Metric Value
eloc 52
c 0
b 0
f 0
dl 0
loc 85
ccs 50
cts 52
cp 0.9615
rs 8.7361
cc 5
nc 9
nop 2
crap 5.0014

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $certificates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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