Passed
Pull Request — master (#61)
by
unknown
12:50
created

SignedElementTest   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 1
Metric Value
wmc 10
eloc 120
c 7
b 0
f 1
dl 0
loc 231
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\Test\XML;
6
7
use DOMElement;
8
use PHPUnit\Framework\Attributes\CoversClass;
9
use PHPUnit\Framework\TestCase;
10
use SimpleSAML\XML\DOMDocumentFactory;
11
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
12
use SimpleSAML\XMLSecurity\Constants as C;
13
use SimpleSAML\XMLSecurity\CryptoEncoding\PEM;
14
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
15
use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException;
16
use SimpleSAML\XMLSecurity\Key\PublicKey;
17
use SimpleSAML\XMLSecurity\Key\X509Certificate;
18
use SimpleSAML\XMLSecurity\Test\XML\CustomSignable;
19
use SimpleSAML\XMLSecurity\TestUtils\PEMCertificatesMock;
20
use SimpleSAML\XMLSecurity\XML\ds\Signature;
21
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;
22
23
use function dirname;
24
use function strval;
25
26
/**
27
 * Class \SimpleSAML\XMLSecurity\Test\XML\SignedElementTest
28
 *
29
 * @package simplesamlphp/xml-security
30
 */
31
#[CoversClass(SignedElementTrait::class)]
32
#[CoversClass(CustomSignable::class)]
33
final class SignedElementTest extends TestCase
34
{
35
    /** @var \SimpleSAML\XMLSecurity\CryptoEncoding\PEM */
36
    private PEM $certificate;
37
38
    /** @var \SimpleSAML\XMLSecurity\CryptoEncoding\PEM */
39
    private PEM $wrong_certificate;
40
41
    /** @var \DOMElement */
42
    private DOMElement $signedDocumentWithComments;
43
44
    /** @var \DOMElement */
45
    private DOMElement $signedDocument;
46
47
    /** @var \DOMElement  */
48
    private DOMElement $tamperedDocument;
49
50
51
    /**
52
     */
53
    public function setUp(): void
54
    {
55
        $this->signedDocumentWithComments = DOMDocumentFactory::fromFile(
56
            dirname(__FILE__, 2) . '/resources/xml/custom_CustomSignableSignedWithComments.xml',
57
        )->documentElement;
58
59
        $this->signedDocument = DOMDocumentFactory::fromFile(
60
            dirname(__FILE__, 2) . '/resources/xml/custom_CustomSignableSigned.xml',
61
        )->documentElement;
62
63
        $this->tamperedDocument = DOMDocumentFactory::fromFile(
64
            dirname(__FILE__, 2) . '/resources/xml/custom_CustomSignableSignedTampered.xml',
65
        )->documentElement;
66
67
        $this->certificate = PEM::fromString(
68
            PEMCertificatesMock::getPlainCertificate(PEMCertificatesMock::SELFSIGNED_CERTIFICATE),
69
        );
70
71
        $this->wrong_certificate = PEM::fromString(
72
            PEMCertificatesMock::getPlainCertificate(PEMCertificatesMock::OTHER_CERTIFICATE),
73
        );
74
    }
75
76
77
    /**
78
     * Test creating a signed object from its XML representation.
79
     */
80
    public function testUnmarshalling(): void
81
    {
82
        $customSigned = CustomSignable::fromXML($this->signedDocument);
83
84
        $this->assertEquals(
85
            $this->signedDocument->ownerDocument->saveXML($this->signedDocument),
86
            strval($customSigned),
87
        );
88
    }
89
90
91
    /**
92
     * Test the verification of a signature with a given key.
93
     */
94
    public function testSuccessfulVerifyingWithGivenKey(): void
95
    {
96
        $customSigned = CustomSignable::fromXML($this->signedDocument);
97
98
        $this->assertTrue($customSigned->isSigned());
99
        $signature = $customSigned->getSignature();
100
        $this->assertInstanceOf(Signature::class, $signature);
101
        $sigAlg = $signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
102
        $this->assertEquals(C::SIG_RSA_SHA256, $sigAlg);
103
        $factory = new SignatureAlgorithmFactory();
104
        $certificate = new X509Certificate($this->certificate);
105
        $verifier = $factory->getAlgorithm($sigAlg, $certificate->getPublicKey());
106
107
        $verified = $customSigned->verify($verifier);
108
        $this->assertInstanceOf(CustomSignable::class, $verified);
109
        $this->assertFalse($verified->isSigned());
110
        $this->assertEquals(
111
            '<ssp:CustomSignable xmlns:ssp="urn:x-simplesamlphp:namespace"><ssp:Chunk>Some' .
112
            '</ssp:Chunk></ssp:CustomSignable>',
113
            strval($verified),
114
        );
115
        $this->assertEquals($certificate->getPublicKey(), $verified->getVerifyingKey());
116
    }
117
118
119
    /**
120
     * Test the verification of a signature with the wrong key first, and the right one second.
121
     * See Github issue #51
122
     */
123
    public function testSuccessfulVerifyingWithWrongKeyFirstRightOneSecond(): void
124
    {
125
        $customSigned = CustomSignable::fromXML($this->signedDocument);
126
127
        $this->assertTrue($customSigned->isSigned());
128
        $signature = $customSigned->getSignature();
129
        $this->assertInstanceOf(Signature::class, $signature);
130
        $sigAlg = $signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
131
        $this->assertEquals(C::SIG_RSA_SHA256, $sigAlg);
132
133
        $verified = null;
134
        foreach ([$this->wrong_certificate, $this->certificate] as $i => $key) {
135
            $factory = new SignatureAlgorithmFactory();
136
            $certificate = new X509Certificate($key);
137
            $verifier = $factory->getAlgorithm($sigAlg, $certificate->getPublicKey());
138
139
            try {
140
                $verified = $customSigned->verify($verifier);
141
                break 1;
142
            } catch (SignatureVerificationFailedException $e) {
143
                continue;
144
            }
145
        }
146
147
        $this->assertInstanceOf(CustomSignable::class, $verified);
148
        $this->assertFalse($verified->isSigned());
149
        $this->assertEquals(
150
            '<ssp:CustomSignable xmlns:ssp="urn:x-simplesamlphp:namespace"><ssp:Chunk>Some' .
151
            '</ssp:Chunk></ssp:CustomSignable>',
152
            strval($verified),
153
        );
154
        $this->assertEquals($certificate->getPublicKey(), $verified->getVerifyingKey());
155
    }
156
157
158
    /**
159
     * Test the verification of a signature without passing a key, just what's in KeyInfo
160
     */
161
    public function testSuccessfulVerifyingWithoutKey(): void
162
    {
163
        $customSigned = CustomSignable::fromXML($this->signedDocument);
164
165
        $this->assertTrue($customSigned->isSigned());
166
        $signature = $customSigned->getSignature();
167
        $this->assertInstanceOf(Signature::class, $signature);
168
        $sigAlg = $signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
169
        $this->assertEquals(C::SIG_RSA_SHA256, $sigAlg);
170
        $certificate = new X509Certificate($this->certificate);
171
172
        $verified = $customSigned->verify();
173
        $this->assertInstanceOf(CustomSignable::class, $verified);
174
        $this->assertFalse($verified->isSigned());
175
        $this->assertEquals(
176
            '<ssp:CustomSignable xmlns:ssp="urn:x-simplesamlphp:namespace"><ssp:Chunk>Some' .
177
            '</ssp:Chunk></ssp:CustomSignable>',
178
            strval($verified),
179
        );
180
        $validatingKey = $verified->getVerifyingKey();
181
        $this->assertInstanceOf(PublicKey::class, $validatingKey);
182
        $this->assertEquals($certificate->getPublicKey(), $validatingKey);
183
    }
184
185
186
    /**
187
     * Test that verifying a tampered signature, without giving a key for verification, fails as expected.
188
     */
189
    public function testVerifyingTamperedSignatureWithoutKeyFails(): void
190
    {
191
        $customSigned = CustomSignable::fromXML($this->tamperedDocument);
192
193
        $this->assertTrue($customSigned->isSigned());
194
        $signature = $customSigned->getSignature();
195
        $this->assertInstanceOf(Signature::class, $signature);
196
        $sigAlg = $signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
197
        $this->assertEquals(C::SIG_RSA_SHA256, $sigAlg);
198
199
        $this->expectException(RuntimeException::class);
200
        $this->expectExceptionMessage('Failed to verify signature.');
201
        $customSigned->verify();
202
    }
203
204
205
    /**
206
     * Test that verifying a tampered signature with a given key fails as expected.
207
     */
208
    public function testVerifyingTamperedSignatureWithKeyFails(): void
209
    {
210
        $customSigned = CustomSignable::fromXML($this->tamperedDocument);
211
212
        $this->assertTrue($customSigned->isSigned());
213
        $signature = $customSigned->getSignature();
214
        $this->assertInstanceOf(Signature::class, $signature);
215
        $sigAlg = $signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
216
        $this->assertEquals(C::SIG_RSA_SHA256, $sigAlg);
217
        $factory = new SignatureAlgorithmFactory();
218
        $certificate = new X509Certificate($this->certificate);
219
        $verifier = $factory->getAlgorithm($sigAlg, $certificate->getPublicKey());
220
221
        $this->expectException(RuntimeException::class);
222
        $this->expectExceptionMessage('Failed to verify signature.');
223
        $customSigned->verify($verifier);
224
    }
225
226
227
228
    /**
229
     * Test the verification of a signature with a given key, for an element that has comments in it.
230
     *
231
     * In this case, canonicalization must remove the comments, and the object resulting the verification must NOT
232
     * have them.
233
     */
234
    public function testSuccessfulVerifyingDocumentWithComments(): void
235
    {
236
        $customSigned = CustomSignable::fromXML($this->signedDocumentWithComments);
237
238
        $this->assertTrue($customSigned->isSigned());
239
        $signature = $customSigned->getSignature();
240
        $this->assertInstanceOf(Signature::class, $signature);
241
        $sigAlg = $signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
242
        $this->assertEquals(C::SIG_RSA_SHA256, $sigAlg);
243
        $factory = new SignatureAlgorithmFactory();
244
        $certificate = new X509Certificate($this->certificate);
245
        $verifier = $factory->getAlgorithm($sigAlg, $certificate->getPublicKey());
246
247
        // verify first that our dumb object normally retains comments
248
        $this->assertEquals(
249
            $this->signedDocumentWithComments->ownerDocument->saveXML($this->signedDocumentWithComments),
250
            strval($customSigned),
251
        );
252
253
        $verified = $customSigned->verify($verifier);
254
        $this->assertInstanceOf(CustomSignable::class, $verified);
255
        $this->assertFalse($verified->isSigned());
256
        $this->assertEquals(
257
            '<ssp:CustomSignable xmlns:ssp="urn:x-simplesamlphp:namespace"><ssp:Chunk><!--comment-->Some' .
258
            '<!--comment--></ssp:Chunk></ssp:CustomSignable>',
259
            strval($verified),
260
        );
261
        $this->assertEquals($certificate->getPublicKey(), $verified->getVerifyingKey());
262
    }
263
}
264