Passed
Push — master ( a9b6b0...8ce621 )
by Tim
02:02
created

SignedElementTrait::getValidatingKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\XML;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\XML\DOMDocumentFactory;
9
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
10
use SimpleSAML\XMLSecurity\Alg\SignatureAlgorithm;
11
use SimpleSAML\XMLSecurity\Constants as C;
12
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
13
use SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException;
14
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
15
use SimpleSAML\XMLSecurity\Key;
16
use SimpleSAML\XMLSecurity\Utils\Security;
17
use SimpleSAML\XMLSecurity\Utils\XML;
18
use SimpleSAML\XMLSecurity\Utils\XPath;
19
use SimpleSAML\XMLSecurity\XML\ds\Reference;
20
use SimpleSAML\XMLSecurity\XML\ds\Signature;
21
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
22
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
23
24
use function array_pop;
25
use function base64_decode;
26
use function in_array;
27
28
/**
29
 * Helper trait for processing signed elements.
30
 *
31
 * @package simplesamlphp/xml-security
32
 */
33
trait SignedElementTrait
34
{
35
    use CanonicalizableElementTrait;
36
37
    /**
38
     * The signature of this element.
39
     *
40
     * @var \SimpleSAML\XMLSecurity\XML\ds\Signature|null $signature
41
     */
42
    protected ?Signature $signature = null;
43
44
    /**
45
     * The key that successfully verifies the signature in this object.
46
     *
47
     * @var \SimpleSAML\XMLSecurity\Key\AbstractKey|null
48
     */
49
    private ?Key\AbstractKey $validatingKey = null;
50
51
52
    /**
53
     * Get the signature element of this object.
54
     *
55
     * @return \SimpleSAML\XMLSecurity\XML\ds\Signature
56
     */
57
    public function getSignature(): ?Signature
58
    {
59
        return $this->signature;
60
    }
61
62
63
    /**
64
     * Initialize a signed element from XML.
65
     *
66
     * @param \SimpleSAML\XMLSecurity\XML\ds\Signature $signature The ds:Signature object
67
     */
68
    protected function setSignature(Signature $signature): void
69
    {
70
        $this->signature = $signature;
71
    }
72
73
74
    /**
75
     * Make sure the given Reference points to the original XML given.
76
     */
77
    private function validateReferenceUri(Reference $reference, \DOMElement $xml): void
78
    {
79
        if (
80
            in_array(
81
                $this->signature->getSignedInfo()->getCanonicalizationMethod()->getAlgorithm(),
0 ignored issues
show
Bug introduced by
The method getSignedInfo() does not exist on null. ( Ignorable by Annotation )

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

81
                $this->signature->/** @scrutinizer ignore-call */ 
82
                                  getSignedInfo()->getCanonicalizationMethod()->getAlgorithm(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
82
                [
83
                    C::C14N_INCLUSIVE_WITH_COMMENTS,
84
                    C::C14N_EXCLUSIVE_WITH_COMMENTS,
85
                ]
86
            )
87
            && !$reference->isXPointer()
88
        ) { // canonicalization with comments used, but reference wasn't an xpointer!
89
            throw new RuntimeException('Invalid reference for canonicalization algorithm.');
90
        }
91
92
        $id = $this->getId();
93
        $uri = $reference->getURI();
94
95
        if (empty($uri) || $uri === '#xpointer(/)') { // same-document reference
96
            Assert::true(
97
                $xml->isSameNode($xml->ownerDocument->documentElement),
98
                'Cannot use document reference when element is not the root of the document.',
99
                RuntimeException::class
100
            );
101
        } else { // short-name or scheme-based xpointer
102
            Assert::notEmpty(
103
                $id,
104
                'Reference points to an element, but given element does not have an ID.',
105
                RuntimeException::class
106
            );
107
            Assert::oneOf(
108
                $uri,
109
                [
110
                    '#' . $id,
111
                    '#xpointer(id(' . $id . '))'
112
                ],
113
                'Reference does not point to given element.',
114
                RuntimeException::class
115
            );
116
        }
117
    }
118
119
120
    /**
121
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface
122
     */
123
    private function validateReference(): SignedElementInterface
124
    {
125
        /** @var \SimpleSAML\XMLSecurity\XML\ds\Signature $this->signature */
126
        $signedInfo = $this->signature->getSignedInfo();
127
        $references = $signedInfo->getReferences();
128
        Assert::count(
129
            $references,
130
            1,
131
            'Exactly one reference expected in signature.',
132
            RuntimeException::class
133
        );
134
        $reference = array_pop($references);
135
136
        $xml = $this->getOriginalXML();
137
        $this->validateReferenceUri($reference, $xml);
138
139
        $xp = XPath::getXPath($xml->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $xml->ownerDocument can also be of type null; however, parameter $node of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMNode, 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

139
        $xp = XPath::getXPath(/** @scrutinizer ignore-type */ $xml->ownerDocument);
Loading history...
140
        $sigNode = XPath::xpQuery($xml, 'child::ds:Signature', $xp);
141
        Assert::count(
142
            $sigNode,
143
            1,
144
            'None or more than one signature found in object.',
145
            RuntimeException::class
146
        );
147
        $xml->removeChild($sigNode[0]);
148
149
        $data = XML::processTransforms($reference->getTransforms(), $xml);
0 ignored issues
show
Bug introduced by
It seems like $reference->getTransforms() can also be of type null; however, parameter $transforms of SimpleSAML\XMLSecurity\U...ML::processTransforms() does only seem to accept SimpleSAML\XMLSecurity\XML\ds\Transforms, 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

149
        $data = XML::processTransforms(/** @scrutinizer ignore-type */ $reference->getTransforms(), $xml);
Loading history...
150
        $digest = Security::hash($reference->getDigestMethod()->getAlgorithm(), $data, false);
151
152
        if (Security::compareStrings($digest, base64_decode($reference->getDigestValue()->getRawContent())) !== true) {
153
            throw new RuntimeException('Failed to verify signature.');
154
        }
155
156
        $verifiedXml = DOMDocumentFactory::fromString($data);
157
        return static::fromXML($verifiedXml->documentElement);
158
    }
159
160
161
    /**
162
     * Verify this element against a public key.
163
     *
164
     * true is returned on success, false is returned if we don't have any
165
     * signature we can verify. An exception is thrown if the signature
166
     * validation fails.
167
     *
168
     * @param \SimpleSAML\XMLSecurity\Alg\SignatureAlgorithm|null $verifier The verifier to use to verify the signature.
169
     * If null, attempt to verify it with the KeyInfo information in the signature.
170
     *
171
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The Signed element if it was verified.
172
     */
173
    private function verifyInternal(SignatureAlgorithm $verifier): SignedElementInterface
174
    {
175
        /** @var \SimpleSAML\XMLSecurity\XML\ds\Signature $this->signature */
176
        $signedInfo = $this->signature->getSignedInfo();
177
        $c14nAlg = $signedInfo->getCanonicalizationMethod()->getAlgorithm();
178
        $c14nSignedInfo = $signedInfo->canonicalize($c14nAlg);
179
        /** @var SignedElementInterface $ref */
180
        $ref = $this->validateReference();
181
182
        if (
183
            $verifier->verify(
184
                $c14nSignedInfo, // the canonicalized ds:SignedInfo element (plaintext)
185
                base64_decode($this->signature->getSignatureValue()->getRawContent()) // the actual signature
186
            )
187
        ) {
188
            /*
189
             * validateReference() returns an object of the same class using this trait. This means the validatingKey
190
             * property is available, and we can set it on the newly created object because we are in the same class,
191
             * even thought the property itself is private.
192
             */
193
            /** @psalm-suppress NoInterfaceProperties */
194
            $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...
195
            return $ref;
196
        }
197
        throw new RuntimeException('Failed to verify signature.');
198
    }
199
200
201
    /**
202
     * Retrieve certificates that sign this element.
203
     *
204
     * @return \SimpleSAML\XMLSecurity\Key\AbstractKey|null The key that successfully verified this signature.
205
     */
206
    public function getVerifyingKey(): ?Key\AbstractKey
207
    {
208
        return $this->validatingKey;
209
    }
210
211
212
    /**
213
     * Whether this object is signed or not.
214
     *
215
     * @return bool
216
     */
217
    public function isSigned(): bool
218
    {
219
        return $this->signature !== null;
220
    }
221
222
223
    /**
224
     * Verify the signature in this object.
225
     *
226
     * If no signature is present, false is returned. If a signature is present,
227
     * but cannot be verified, an exception will be thrown.
228
     *
229
     * @param \SimpleSAML\XMLSecurity\Alg\SignatureAlgorithm|null $verifier The verifier to use to verify the signature.
230
     * If null, attempt to verify it with the KeyInfo information in the signature.
231
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The object processed again from its canonicalised
232
     * representation verified by the signature.
233
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException if the object is not signed.
234
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException if no key is passed and there is no KeyInfo
235
     * in the signature.
236
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException if the signature fails to verify.
237
     */
238
    public function verify(SignatureAlgorithm $verifier = null): SignedElementInterface
239
    {
240
        if (!$this->isSigned()) {
241
            throw new NoSignatureFoundException();
242
        }
243
244
        $keyInfo = $this->signature->getKeyInfo();
245
        $algId = $this->signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
246
        if ($verifier === null && $keyInfo === null) {
247
            throw new InvalidArgumentException('No key or KeyInfo available for signature verification.');
248
        }
249
250
        if ($verifier !== null) {
251
            // verify using given key
252
            // TODO: make this part of the condition, so that we support using this verifier to decrypt an encrypted key
253
            Assert::eq(
254
                $verifier->getAlgorithmId(),
255
                $algId,
256
                'Algorithm provided in key does not match algorithm used in signature.'
257
            );
258
259
            return $this->verifyInternal($verifier);
260
        }
261
262
        $factory = new SignatureAlgorithmFactory();
263
        foreach ($keyInfo->getInfo() as $info) {
264
            if (!$info instanceof X509Data) {
265
                continue;
266
            }
267
268
            /** @var \SimpleSAML\XMLSecurity\XML\ds\X509Data $info */
269
            foreach ($info->getData() as $data) {
270
                if (!$data instanceof X509Certificate) {
271
                    // not supported
272
                    continue;
273
                }
274
275
                // build a valid PEM for the certificate
276
                $cert = Key\X509Certificate::PEM_HEADER . "\n" .
277
                        $data->getRawContent() . "\n" .
278
                        Key\X509Certificate::PEM_FOOTER;
279
280
                $key = new Key\X509Certificate($cert);
281
                $verifier = $factory->getAlgorithm($algId, $key);
282
283
                try {
284
                    return $this->verifyInternal($verifier);
285
                } catch (RuntimeException $e) {
286
                    // failed to verify with this certificate, try with other, if any
287
                }
288
            }
289
        }
290
        throw new RuntimeException('Failed to verify signature.');
291
    }
292
293
294
    /**
295
     * @return string|null
296
     */
297
    abstract public function getId(): ?string;
298
}
299