Passed
Pull Request — master (#2)
by Jaime Pérez
02:11
created

SignedElementTrait::setSignature()   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
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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;
12
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
13
use SimpleSAML\XMLSecurity\Exception\NoSignatureFound;
14
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
15
use SimpleSAML\XMLSecurity\Key\AbstractKey;
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
/**
25
 * Helper trait for processing signed elements.
26
 *
27
 * @package simplesamlphp/xml-security
28
 */
29
trait SignedElementTrait
30
{
31
    use CanonicalizableElementTrait;
32
33
    /**
34
     * The signature of this element.
35
     *
36
     * @var \SimpleSAML\XMLSecurity\XML\ds\Signature|null $signature
37
     */
38
    protected ?Signature $signature = null;
39
40
    /**
41
     * The key that successfully validates the signature in this object.
42
     *
43
     * @var \SimpleSAML\XMLSecurity\Key\AbstractKey|null
44
     */
45
    private ?AbstractKey $validatingKey = null;
46
47
48
    /**
49
     * Get the signature element of this object.
50
     *
51
     * @return \SimpleSAML\XMLSecurity\XML\ds\Signature
52
     */
53
    public function getSignature(): ?Signature
54
    {
55
        return $this->signature;
56
    }
57
58
59
    /**
60
     * Initialize a signed element from XML.
61
     *
62
     * @param \SimpleSAML\XMLSecurity\XML\ds\Signature $signature The ds:Signature object
63
     */
64
    protected function setSignature(Signature $signature): void
65
    {
66
        $this->signature = $signature;
67
    }
68
69
70
    /**
71
     * Make sure the given Reference points to the original XML given.
72
     */
73
    private function validateReferenceUri(Reference $reference, \DOMElement $xml): void
74
    {
75
        if (in_array(
76
                $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

76
                $this->signature->/** @scrutinizer ignore-call */ 
77
                                  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...
77
                [
78
                    Constants::C14N_INCLUSIVE_WITH_COMMENTS,
79
                    Constants::C14N_EXCLUSIVE_WITH_COMMENTS,
80
                ]
81
            ) && !$reference->isXPointer()
82
        ) { // canonicalization with comments used, but reference wasn't an xpointer!
83
            throw new RuntimeException('Invalid reference for canonicalization algorithm.');
84
        }
85
86
        $id = $this->getId();
0 ignored issues
show
Bug introduced by
It seems like getId() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

86
        /** @scrutinizer ignore-call */ 
87
        $id = $this->getId();
Loading history...
87
        $uri = $reference->getURI();
88
89
        if (empty($uri) || $uri === '#xpointer(/)') { // same-document reference
90
            Assert::true(
91
                $xml->isSameNode($xml->ownerDocument->documentElement),
92
                'Cannot use document reference when element is not the root of the document.',
93
                RuntimeException::class
94
95
            );
96
        } else { // short-name or scheme-based xpointer
97
            Assert::notEmpty(
98
                $id,
99
                'Reference points to an element, but given element does not have an ID.',
100
                RuntimeException::class
101
            );
102
            Assert::oneOf(
103
                $uri,
104
                [
105
                    '#' . $id,
106
                    '#xpointer(id(' . $id . '))'
107
                ],
108
                'Reference does not point to given element.',
109
                RuntimeException::class
110
            );
111
        }
112
    }
113
114
115
    /**
116
     * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface
117
     */
118
    private function validateReference(): SignedElementInterface
119
    {
120
        /** @var \SimpleSAML\XMLSecurity\XML\ds\Signature $this->signature */
121
        $signedInfo = $this->signature->getSignedInfo();
122
        $references = $signedInfo->getReferences();
123
        Assert::count(
124
            $references,
125
            1,
126
            'Exactly one reference expected in signature.',
127
            RuntimeException::class
128
        );
129
        $reference = array_pop($references);
130
131
        $xml = $this->getOriginalXML();
132
        $this->validateReferenceUri($reference, $xml);
133
134
        $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 $doc 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

134
        $xp = XPath::getXPath(/** @scrutinizer ignore-type */ $xml->ownerDocument);
Loading history...
135
        $sigNode = $xp->query('child::ds:Signature', $xml);
136
        Assert::count(
137
            $sigNode,
138
            1,
139
            'None or more than one signature found in object.',
140
            RuntimeException::class
141
        );
142
        $xml->removeChild($sigNode->item(0));
143
144
        $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

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