SignedElementTrait::validateReferenceUri()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 38
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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