SignedElementTrait::verify()   B
last analyzed

Complexity

Conditions 10
Paths 7

Size

Total Lines 53
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 28
c 0
b 0
f 0
nc 7
nop 1
dl 0
loc 53
rs 7.6666

How to fix   Long Method    Complexity   

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