Issues (234)

src/XML/SignedElementTrait.php (7 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\XML;
6
7
use DOMElement;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\XML\DOMDocumentFactory;
10
use SimpleSAML\XMLSchema\Exception\TooManyElementsException;
11
use SimpleSAML\XMLSchema\Type\IDValue;
12
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
0 ignored issues
show
The type SimpleSAML\XMLSecurity\A...gnatureAlgorithmFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface;
14
use SimpleSAML\XMLSecurity\Constants as C;
0 ignored issues
show
The type SimpleSAML\XMLSecurity\Constants was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use SimpleSAML\XMLSecurity\CryptoEncoding\PEM;
16
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
17
use SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException;
18
use SimpleSAML\XMLSecurity\Exception\ReferenceValidationFailedException;
19
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
20
use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException;
21
use SimpleSAML\XMLSecurity\Key;
22
use SimpleSAML\XMLSecurity\Key\KeyInterface;
23
use SimpleSAML\XMLSecurity\Utils\XML;
24
use SimpleSAML\XMLSecurity\Utils\XPath;
25
use SimpleSAML\XMLSecurity\XML\ds\Reference;
26
use SimpleSAML\XMLSecurity\XML\ds\Signature;
27
use SimpleSAML\XMLSecurity\XML\ds\SignedInfo;
28
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
0 ignored issues
show
The type SimpleSAML\XMLSecurity\XML\ds\X509Certificate was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
0 ignored issues
show
The type SimpleSAML\XMLSecurity\XML\ds\X509Data was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
31
use function base64_decode;
32
use function hash;
33
use function hash_equals;
34
use function in_array;
35
use function strval;
36
37
/**
38
 * Helper trait for processing signed elements.
39
 *
40
 * @package simplesamlphp/xml-security
41
 *
42
 * @phpstan-ignore trait.unused
43
 */
44
trait SignedElementTrait
45
{
46
    use CanonicalizableElementTrait;
47
48
49
    /**
50
     * The signature of this element.
51
     *
52
     * @var \SimpleSAML\XMLSecurity\XML\ds\Signature|null $signature
53
     */
54
    protected ?Signature $signature = null;
55
56
    /**
57
     * The key that successfully verifies the signature in this object.
58
     *
59
     * @var \SimpleSAML\XMLSecurity\Key\KeyInterface|null
60
     */
61
    private ?KeyInterface $validatingKey = null;
62
63
64
    /**
65
     * Get the signature element of this object.
66
     *
67
     * @return \SimpleSAML\XMLSecurity\XML\ds\Signature
68
     */
69
    public function getSignature(): ?Signature
70
    {
71
        return $this->signature;
72
    }
73
74
75
    /**
76
     * Initialize a signed element from XML.
77
     *
78
     * @param \SimpleSAML\XMLSecurity\XML\ds\Signature $signature The ds:Signature object
79
     */
80
    protected function setSignature(Signature $signature): void
81
    {
82
        /**
83
         * By disabling formatting on signed objects, we prevent issues with invalid signatures later on
84
         * at the cost of some whitespace taking up useless bytes in the outputted document.
85
         */
86
        $this->formatOutput = false;
0 ignored issues
show
Bug Best Practice introduced by
The property formatOutput does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
87
        $this->signature = $signature;
88
    }
89
90
91
    /**
92
     * Make sure the given Reference points to the original XML given.
93
     */
94
    private function validateReferenceUri(Reference $reference, DOMElement $xml): void
95
    {
96
        if (
97
            in_array(
98
                $this->getSignature()->getSignedInfo()->getCanonicalizationMethod()->getAlgorithm(),
99
                [
100
                    C::C14N_INCLUSIVE_WITH_COMMENTS,
101
                    C::C14N_EXCLUSIVE_WITH_COMMENTS,
102
                ],
103
            )
104
            && !$reference->isXPointer()
105
        ) { // canonicalization with comments used, but reference wasn't an xpointer!
106
            throw new ReferenceValidationFailedException('Invalid reference for canonicalization algorithm.');
107
        }
108
109
        $id = $this->getId();
110
        $uri = $reference->getURI();
111
112
        if (empty($uri) || $uri->getValue() === '#xpointer(/)') { // same-document reference
113
            Assert::true(
114
                $xml->isSameNode($xml->ownerDocument->documentElement),
115
                'Cannot use document reference when element is not the root of the document.',
116
                ReferenceValidationFailedException::class,
117
            );
118
        } else { // short-name or scheme-based xpointer
119
            Assert::notEmpty(
120
                $id,
121
                'Reference points to an element, but given element does not have an ID.',
122
                ReferenceValidationFailedException::class,
123
            );
124
            Assert::oneOf(
125
                $uri->getValue(),
126
                [
127
                    '#' . $id->getValue(),
128
                    '#xpointer(id(' . $id->getValue() . '))',
129
                ],
130
                'Reference does not point to given element.',
131
                ReferenceValidationFailedException::class,
132
            );
133
        }
134
    }
135
136
137
    /**
138
     * @param \SimpleSAML\XMLSecurity\XML\ds\SignedInfo $signedInfo
139
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface
140
     */
141
    private function validateReference(SignedInfo $signedInfo): SignedElementInterface
142
    {
143
        $xml = $this->getOriginalXML();
144
        $references = $signedInfo->getReferences();
145
        foreach ($references as $reference) {
146
            $this->validateReferenceUri($reference, $xml);
147
        }
148
149
        // Clone the document so we don't mess up the original DOMDocument
150
        $doc = DOMDocumentFactory::create();
151
        $node = $doc->importNode($xml->ownerDocument->documentElement, true);
152
        $doc->appendChild($node);
153
154
        $xp = XPath::getXPath($doc);
155
        $sigNode = XPath::xpQuery($doc->documentElement, 'child::ds:Signature', $xp);
156
        Assert::minCount($sigNode, 1, NoSignatureFoundException::class);
157
        Assert::maxCount($sigNode, 1, 'More than one signature found in object.', TooManyElementsException::class);
158
159
        $doc->documentElement->removeChild($sigNode[0]);
160
        $data = XML::processTransforms($reference->getTransforms(), $doc->documentElement);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $reference seems to be defined by a foreach iteration on line 145. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
161
        $algo = $reference->getDigestMethod()->getAlgorithm()->getValue();
162
        Assert::keyExists(
163
            C::$DIGEST_ALGORITHMS,
164
            $algo,
165
            'Unsupported digest method "' . $algo . '"',
166
            InvalidArgumentException::class,
167
        );
168
169
        $digest = hash(C::$DIGEST_ALGORITHMS[$algo], $data, true);
170
        if (
171
            hash_equals(
172
                $digest,
173
                base64_decode($reference->getDigestValue()->getContent()->getValue(), true),
174
            ) !== true
175
        ) {
176
            throw new SignatureVerificationFailedException('Failed to verify signature.');
177
        }
178
179
        $verifiedXml = DOMDocumentFactory::fromString($data);
180
        return static::fromXML($verifiedXml->documentElement);
181
    }
182
183
184
    /**
185
     * Verify this element against a public key.
186
     *
187
     * true is returned on success, false is returned if we don't have any
188
     * signature we can verify. An exception is thrown if the signature
189
     * validation fails.
190
     *
191
     * @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null $verifier The verifier to use to
192
     * verify the signature. If null, attempt to verify it with the KeyInfo information in the signature.
193
     *
194
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The Signed element if it was verified.
195
     */
196
    #[\NoDiscard]
197
    private function verifyInternal(SignatureAlgorithmInterface $verifier): SignedElementInterface
198
    {
199
        /** @var \SimpleSAML\XMLSecurity\XML\ds\Signature $this->signature */
200
        $signedInfo = $this->getSignature()->getSignedInfo();
201
        $c14nAlg = $signedInfo->getCanonicalizationMethod()->getAlgorithm();
202
203
        // the canonicalized ds:SignedInfo element (plaintext)
204
        $c14nSignedInfo = $signedInfo->canonicalize($c14nAlg->getValue());
205
        $ref = $this->validateReference(
206
            SignedInfo::fromXML(DOMDocumentFactory::fromString($c14nSignedInfo)->documentElement),
207
        );
208
209
        if (
210
            $verifier?->verify(
211
                $c14nSignedInfo, // the canonicalized ds:SignedInfo element (plaintext)
212
                // the actual signature
213
                base64_decode(strval($this->getSignature()->getSignatureValue()->getValue()), true),
214
            )
215
        ) {
216
            /*
217
             * validateReference() returns an object of the same class using this trait. This means the validatingKey
218
             * property is available, and we can set it on the newly created object because we are in the same class,
219
             * even thought the property itself is private.
220
             */
221
            $ref->validatingKey = $verifier->getKey();
0 ignored issues
show
Accessing validatingKey on the interface SimpleSAML\XMLSecurity\XML\SignedElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
222
            return $ref;
223
        }
224
        throw new SignatureVerificationFailedException('Failed to verify signature.');
225
    }
226
227
228
    /**
229
     * Retrieve certificates that sign this element.
230
     *
231
     * @return \SimpleSAML\XMLSecurity\Key\KeyInterface|null The key that successfully verified this signature.
232
     */
233
    public function getVerifyingKey(): ?KeyInterface
234
    {
235
        return $this->validatingKey;
236
    }
237
238
239
    /**
240
     * Whether this object is signed or not.
241
     */
242
    public function isSigned(): bool
243
    {
244
        return $this->signature !== null;
245
    }
246
247
248
    /**
249
     * Verify the signature in this object.
250
     *
251
     * If no signature is present, false is returned. If a signature is present,
252
     * but cannot be verified, an exception will be thrown.
253
     *
254
     * @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null $verifier The verifier to use to
255
     *  verify the signature. If null, attempt to verify it with the KeyInfo information in the signature.
256
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The object processed again from its canonicalised
257
     *  representation verified by the signature.
258
     *
259
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException if the object is not signed.
260
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException if no key is passed and there is no KeyInfo
261
     * in the signature.
262
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException if the signature fails to verify.
263
     */
264
    #[\NoDiscard]
265
    public function verify(?SignatureAlgorithmInterface $verifier = null): SignedElementInterface
266
    {
267
        if (!$this->isSigned()) {
268
            throw new NoSignatureFoundException();
269
        }
270
271
        $keyInfo = $this->signature?->getKeyInfo();
272
        $algId = $this->signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
273
        if ($verifier === null && $keyInfo === null) {
274
            throw new InvalidArgumentException('No key or KeyInfo available for signature verification.');
275
        }
276
277
        if ($verifier !== null) {
278
            // verify using given key
279
            // TODO: make this part of the condition, so that we support using this verifier to decrypt an encrypted key
280
            Assert::eq(
281
                $verifier->getAlgorithmId(),
282
                $algId,
283
                'Algorithm provided in key does not match algorithm used in signature.',
284
            );
285
286
            return $this->verifyInternal($verifier);
287
        }
288
289
        $factory = new SignatureAlgorithmFactory();
290
        foreach ($keyInfo->getInfo() as $info) {
291
            if (!$info instanceof X509Data) {
292
                continue;
293
            }
294
295
            foreach ($info->getData() as $data) {
296
                if (!$data instanceof X509Certificate) {
297
                    // not supported
298
                    continue;
299
                }
300
301
                // build a valid PEM for the certificate
302
                $cert = sprintf(
303
                    "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----",
304
                    strval($data->getContent()),
305
                );
306
307
                $cert = new Key\X509Certificate(PEM::fromString($cert));
308
                $verifier = $factory->getAlgorithm($algId->getValue(), $cert->getPublicKey());
309
310
                try {
311
                    return $this->verifyInternal($verifier);
312
                } catch (RuntimeException) {
313
                    // failed to verify with this certificate, try with other, if any
314
                }
315
            }
316
        }
317
        throw new SignatureVerificationFailedException('Failed to verify signature.');
318
    }
319
320
321
    /**
322
     * @return \SimpleSAML\XML\Type\IDValue|null
323
     */
324
    abstract public function getId(): ?IDValue;
325
326
327
    /**
328
     * Get the list of algorithms that are blacklisted for any signing operation.
329
     *
330
     * @return string[]|null An array with all algorithm identifiers that are blacklisted, or null to use this
331
     * libraries default.
332
     */
333
    abstract public function getBlacklistedAlgorithms(): ?array;
334
}
335