Completed
Push — master ( 9da07f...da47a3 )
by Daniel
01:23
created

XmlSigner::setReferenceUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Selective\XmlDSig;
4
5
use DOMDocument;
6
use Selective\XmlDSig\Exception\XmlSignerException;
7
8
/**
9
 * Sign XML Documents with Digital Signatures (XMLDSIG).
10
 */
11
final class XmlSigner
12
{
13
    //
14
    // RSA (PKCS#1 v1.5) Identifier
15
    // https://www.w3.org/TR/xmldsig-core/#sec-PKCS1
16
    //
17
    private const SHA1_URL = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
18
    private const SHA224_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224';
19
    private const SHA256_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
20
    private const SHA384_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384';
21
    private const SHA512_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512';
22
23
    /**
24
     * @var int
25
     */
26
    private $digestAlgorithm;
27
28
    /**
29
     * @var string
30
     */
31
    private $digestAlgorithmName;
32
33
    /**
34
     * @var string
35
     */
36
    private $digestAlgorithmUrl;
37
38
    /**
39
     * @var resource|false
40
     */
41
    private $privateKeyId;
42
43
    /**
44
     * @var string
45
     */
46
    private $referenceUri = '';
47
48
    /**
49
     * Read and load the pfx file.
50
     *
51
     * @param string $filename PFX filename
52
     * @param string $password PFX password
53
     *
54
     * @throws XmlSignerException
55
     *
56
     * @return bool Success
57
     */
58 1
    public function loadPfx(string $filename, string $password): bool
59
    {
60 1
        if (!file_exists($filename)) {
61
            throw new XmlSignerException(sprintf('File not found: %s', $filename));
62
        }
63
64 1
        $certStore = file_get_contents($filename);
65
66 1
        if (!$certStore) {
67
            throw new XmlSignerException(sprintf('File could not be read: %s', $filename));
68
        }
69
70 1
        $status = openssl_pkcs12_read($certStore, $certInfo, $password);
71
72 1
        if (!$status) {
73
            throw new XmlSignerException('Invalid PFX pasword');
74
        }
75
76
        // Read the private key
77 1
        $this->privateKeyId = openssl_get_privatekey((string)$certInfo['pkey']);
78
79 1
        if (!$this->privateKeyId) {
80
            throw new XmlSignerException('Invalid private key');
81
        }
82
83 1
        return true;
84
    }
85
86
    /**
87
     * Sign an XML file and save the signature in a new file.
88
     * This method does not save the public key within the XML file.
89
     *
90
     * @param string $filename Input file
91
     * @param string $outputFilename Output file
92
     * @param string $digestAlgorithm For example: sha1, sha224, sha256, sha384, sha512
93
     *
94
     * @throws XmlSignerException
95
     *
96
     * @return bool Success
97
     */
98 1
    public function signXmlFile(string $filename, string $outputFilename, string $digestAlgorithm): bool
99
    {
100 1
        if (!file_exists($filename)) {
101
            throw new XmlSignerException(sprintf('File not found: %s', $filename));
102
        }
103
104 1
        if (!$this->privateKeyId) {
105
            throw new XmlSignerException('No private key provided');
106
        }
107
108 1
        $this->setDigestAlgorithm($digestAlgorithm);
109
110
        // Read the xml file content
111 1
        $xml = new DOMDocument();
112 1
        $xml->preserveWhiteSpace = false;
113 1
        $xml->formatOutput = true;
114 1
        $xml->load($filename);
115 1
        $data = $xml->saveXML();
116
117
        // Compute signature with SHA-512
118 1
        $status = openssl_sign($data, $signature, $this->privateKeyId, $this->digestAlgorithm);
119
120 1
        if (!$status) {
121
            throw new XmlSignerException('Computing of the signature failed');
122
        }
123
124
        // Encode signature
125 1
        $signatureValue = base64_encode($signature);
126
127
        // Calculate and encode digest value
128 1
        $digestValue = base64_encode(hash($this->digestAlgorithmName, $data, true));
129
130 1
        $xml = $this->createSignedXml($data, $digestValue, $signatureValue);
131
132 1
        file_put_contents($outputFilename, $xml->saveXML());
133
134 1
        return true;
135
    }
136
137
    /**
138
     * Set reference URI.
139
     *
140
     * @param string $referenceUri The reference URI
141
     *
142
     * @return void
143
     */
144 1
    public function setReferenceUri(string $referenceUri)
145
    {
146 1
        $this->referenceUri = $referenceUri;
147 1
    }
148
149
    /**
150
     * Create the XML representation of the signature.
151
     *
152
     * @param string $data The xml content
153
     * @param string $digestValue The digest value
154
     * @param string $signatureValue The signature
155
     *
156
     * @throws XmlSignerException
157
     *
158
     * @return DOMDocument The DOM document
159
     */
160 1
    private function createSignedXml(string $data, string $digestValue, string $signatureValue): DOMDocument
161
    {
162 1
        $xml = new DOMDocument();
163 1
        $xml->preserveWhiteSpace = false;
164 1
        $xml->formatOutput = true;
165 1
        $isValid = $xml->loadXML($data);
166
167 1
        if (!$isValid || !$xml->documentElement) {
168
            throw new XmlSignerException('Invalid XML content');
169
        }
170
171 1
        $signatureElement = $xml->createElement('Signature');
172 1
        $signatureElement->setAttribute('xmlns', 'http://www.w3.org/2000/09/xmldsig#');
173
174 1
        $signedInfoElement = $xml->createElement('SignedInfo');
175 1
        $signatureElement->appendChild($signedInfoElement);
176
177 1
        $canonicalizationMethodElement = $xml->createElement('CanonicalizationMethod');
178 1
        $canonicalizationMethodElement->setAttribute('Algorithm', 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315');
179 1
        $signedInfoElement->appendChild($canonicalizationMethodElement);
180
181 1
        $signatureMethodElement = $xml->createElement('SignatureMethod');
182 1
        $signatureMethodElement->setAttribute('Algorithm', $this->digestAlgorithmUrl);
183 1
        $signedInfoElement->appendChild($signatureMethodElement);
184
185 1
        $referenceElement = $xml->createElement('Reference');
186 1
        $referenceElement->setAttribute('URI', $this->referenceUri);
187 1
        $signedInfoElement->appendChild($referenceElement);
188
189 1
        $transformsElement = $xml->createElement('Transforms');
190 1
        $referenceElement->appendChild($transformsElement);
191
192 1
        $transformElement = $xml->createElement('Transform');
193 1
        $transformElement->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#enveloped-signature');
194 1
        $transformsElement->appendChild($transformElement);
195
196 1
        $digestMethodElement = $xml->createElement('DigestMethod');
197 1
        $digestMethodElement->setAttribute('Algorithm', $this->digestAlgorithmUrl);
198 1
        $referenceElement->appendChild($digestMethodElement);
199
200 1
        $digestValueElement = $xml->createElement('DigestValue', $digestValue);
201 1
        $referenceElement->appendChild($digestValueElement);
202
203 1
        $signatureValueElement = $xml->createElement('SignatureValue', $signatureValue);
204 1
        $signatureElement->appendChild($signatureValueElement);
205
206
        // Append the element to the XML document.
207
        // We insert the new element as root (child of the document)
208 1
        $xml->documentElement->appendChild($signatureElement);
209
210 1
        return $xml;
211
    }
212
213
    /**
214
     * Set digest algorithm.
215
     *
216
     * @param string $digestAlgorithm For example: sha1, sha224, sha256, sha384, sha512
217
     */
218 1
    private function setDigestAlgorithm(string $digestAlgorithm): void
219
    {
220 1
        switch ($digestAlgorithm) {
221 1
            case 'sha1':
222 1
                $this->digestAlgorithmUrl = self::SHA1_URL;
223 1
                $this->digestAlgorithm = OPENSSL_ALGO_SHA1;
224 1
                break;
225 1
            case 'sha224':
226 1
                $this->digestAlgorithmUrl = self::SHA224_URL;
227 1
                $this->digestAlgorithm = OPENSSL_ALGO_SHA224;
228 1
                break;
229 1
            case 'sha256':
230 1
                $this->digestAlgorithmUrl = self::SHA256_URL;
231 1
                $this->digestAlgorithm = OPENSSL_ALGO_SHA256;
232 1
                break;
233 1
            case 'sha384':
234 1
                $this->digestAlgorithmUrl = self::SHA384_URL;
235 1
                $this->digestAlgorithm = OPENSSL_ALGO_SHA384;
236 1
                break;
237 1
            case 'sha512':
238 1
                $this->digestAlgorithmUrl = self::SHA512_URL;
239 1
                $this->digestAlgorithm = OPENSSL_ALGO_SHA512;
240 1
                break;
241
            default:
242
                throw new XmlSignerException("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>");
243
        }
244
245 1
        $this->digestAlgorithmName = $digestAlgorithm;
246 1
    }
247
248
    /**
249
     * Destructor.
250
     */
251 2
    public function __destruct()
252
    {
253
        // Free the key from memory
254 2
        if ($this->privateKeyId) {
255 1
            openssl_free_key($this->privateKeyId);
0 ignored issues
show
Bug introduced by
It seems like $this->privateKeyId can also be of type true; however, parameter $key_identifier of openssl_free_key() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
            openssl_free_key(/** @scrutinizer ignore-type */ $this->privateKeyId);
Loading history...
256
        }
257 2
    }
258
}
259