MetadataBuilder::buildKeyDescriptor()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 16
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\adfs\IdP;
6
7
use Beste\Clock\LocalizedClock;
8
use Exception;
9
use Psr\Clock\ClockInterface;
10
use SimpleSAML\Assert\Assert;
11
use SimpleSAML\Configuration;
12
use SimpleSAML\Logger;
13
use SimpleSAML\Module;
14
use SimpleSAML\SAML2\Exception\ArrayValidationException;
15
use SimpleSAML\SAML2\XML\md\AbstractMetadataDocument;
16
use SimpleSAML\SAML2\XML\md\ContactPerson;
17
use SimpleSAML\SAML2\XML\md\EntityDescriptor;
18
use SimpleSAML\SAML2\XML\md\Extensions;
19
use SimpleSAML\SAML2\XML\md\KeyDescriptor;
20
use SimpleSAML\SAML2\XML\md\Organization;
21
use SimpleSAML\SAML2\XML\mdattr\EntityAttributes;
22
use SimpleSAML\SAML2\XML\mdrpi\RegistrationInfo;
23
use SimpleSAML\SAML2\XML\mdui\DiscoHints;
24
use SimpleSAML\SAML2\XML\mdui\UIInfo;
25
use SimpleSAML\SAML2\XML\saml\Attribute;
26
use SimpleSAML\SAML2\XML\saml\AttributeValue;
27
use SimpleSAML\SAML2\XML\shibmd\Scope;
28
use SimpleSAML\Utils;
29
use SimpleSAML\WSSecurity\Constants as C;
30
use SimpleSAML\WSSecurity\XML\fed\PassiveRequestorEndpoint;
31
use SimpleSAML\WSSecurity\XML\fed\SecurityTokenServiceEndpoint;
32
use SimpleSAML\WSSecurity\XML\fed\SecurityTokenServiceType;
33
use SimpleSAML\WSSecurity\XML\fed\TokenType;
34
use SimpleSAML\WSSecurity\XML\fed\TokenTypesOffered;
35
use SimpleSAML\WSSecurity\XML\wsa_200508\Address;
36
use SimpleSAML\WSSecurity\XML\wsa_200508\EndpointReference;
37
use SimpleSAML\XML\Chunk;
38
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
39
use SimpleSAML\XMLSecurity\Key\PrivateKey;
40
use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
41
use SimpleSAML\XMLSecurity\XML\ds\KeyName;
42
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
43
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
44
45
use function array_key_exists;
46
use function preg_match;
47
48
/**
49
 * Common code for building SAML 2 metadata based on the available configuration.
50
 *
51
 * @package simplesamlphp/simplesamlphp-module-adfs
52
 */
53
class MetadataBuilder
54
{
55
    /** @var \Psr\Clock\ClockInterface */
56
    protected ClockInterface $clock;
57
58
59
    /**
60
     * Constructor.
61
     *
62
     * @param \SimpleSAML\Configuration $config The general configuration
63
     * @param \SimpleSAML\Configuration $metadata The metadata configuration
64
     */
65
    public function __construct(
66
        protected Configuration $config,
67
        protected Configuration $metadata,
68
    ) {
69
        $this->clock = LocalizedClock::in('Z');
70
    }
71
72
73
    /**
74
     * Build a metadata document
75
     *
76
     * @return \SimpleSAML\SAML2\XML\md\EntityDescriptor
77
     */
78
    public function buildDocument(): EntityDescriptor
79
    {
80
        $entityId = $this->metadata->getString('entityid');
81
        $contactPerson = $this->getContactPerson();
82
        $organization = $this->getOrganization();
83
        $roleDescriptor = $this->getRoleDescriptor();
84
85
        $randomUtils = new Utils\Random();
86
        $entityDescriptor = new EntityDescriptor(
87
            id: $randomUtils->generateID(),
88
            entityId: $entityId,
89
            contactPerson: $contactPerson,
90
            organization: $organization,
91
            roleDescriptor: $roleDescriptor,
92
        );
93
94
        if ($this->config->getOptionalBoolean('metadata.sign.enable', false) === true) {
95
            $this->signDocument($entityDescriptor);
96
        }
97
98
        return $entityDescriptor;
99
    }
100
101
102
    /**
103
     * @param \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument $document
104
     * @return \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument
105
     */
106
    protected function signDocument(AbstractMetadataDocument $document): AbstractMetadataDocument
107
    {
108
        $cryptoUtils = new Utils\Crypto();
109
110
        /** @var array<mixed> $keyArray */
111
        $keyArray = $cryptoUtils->loadPrivateKey($this->config, true, 'metadata.sign.');
112
        $certArray = $cryptoUtils->loadPublicKey($this->config, false, 'metadata.sign.');
113
        $algo = $this->config->getOptionalString('metadata.sign.algorithm', C::SIG_RSA_SHA256);
114
115
        $key = PrivateKey::fromFile($keyArray['PEM'], $keyArray['password'] ?? '');
116
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm($algo, $key);
0 ignored issues
show
Bug introduced by
It seems like $algo can also be of type null; however, parameter $algId of SimpleSAML\XMLSecurity\A...Factory::getAlgorithm() does only seem to accept string, 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

116
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(/** @scrutinizer ignore-type */ $algo, $key);
Loading history...
117
118
        $keyInfo = null;
119
        if ($certArray !== null) {
120
            $keyInfo = new KeyInfo([
121
                new X509Data([
122
                    new X509Certificate($certArray['certData']),
123
                ]),
124
            ]);
125
        }
126
127
        $document->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
128
        return $document;
129
    }
130
131
132
    /**
133
     * This method builds the md:Organization element, if any
134
     *
135
     * @return \SimpleSAML\SAML2\XML\md\Organization
136
     */
137
    private function getOrganization(): ?Organization
138
    {
139
        if (
140
            !$this->metadata->hasValue('OrganizationName') ||
141
            !$this->metadata->hasValue('OrganizationDisplayName') ||
142
            !$this->metadata->hasValue('OrganizationURL')
143
        ) {
144
            // empty or incomplete organization information
145
            return null;
146
        }
147
148
        $arrayUtils = new Utils\Arrays();
149
        $org = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $org is dead and can be removed.
Loading history...
150
151
        try {
152
            $org = Organization::fromArray([
153
                'OrganizationName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationName'), 'en'),
154
                'OrganizationDisplayName' => $arrayUtils->arrayize(
155
                    $this->metadata->getArray('OrganizationDisplayName'),
156
                    'en',
157
                ),
158
                'OrganizationURL' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationURL'), 'en'),
159
            ]);
160
        } catch (ArrayValidationException $e) {
161
            Logger::error('Federation: invalid content found in contact: ' . $e->getMessage());
162
        }
163
164
        return $org;
165
    }
166
167
168
    /**
169
     * This method builds the role descriptor elements
170
     *
171
     * @return \SimpleSAML\SAML2\XML\md\AbstractRoleDescriptor[]
172
     */
173
    private function getRoleDescriptor(): array
174
    {
175
        $descriptors = [];
176
177
        $set = $this->metadata->getString('metadata-set');
178
        switch ($set) {
179
            case 'adfs-idp-hosted':
180
                $descriptors[] = $this->getSecurityTokenService();
181
                break;
182
            default:
183
                throw new Exception('Not implemented');
184
        }
185
186
        return $descriptors;
187
    }
188
189
190
    /**
191
     * This method builds the SecurityTokenService element
192
     *
193
     * @return \SimpleSAML\WSSecurity\XML\fed\SecurityTokenServiceType
194
     */
195
    public function getSecurityTokenService(): SecurityTokenServiceType
196
    {
197
        $defaultEndpoint = Module::getModuleURL('adfs') . '/idp/prp.php';
198
199
        return new SecurityTokenServiceType(
200
            protocolSupportEnumeration: [C::NS_TRUST_200512, C::NS_TRUST_200502, C::NS_FED],
201
            keyDescriptors: $this->getKeyDescriptor(),
202
            tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]),
203
            securityTokenServiceEndpoint: [
204
                new SecurityTokenServiceEndpoint([
205
                    new EndpointReference(new Address($defaultEndpoint)),
206
                ]),
207
            ],
208
            passiveRequestorEndpoint: [
209
                new PassiveRequestorEndpoint([
210
                    new EndpointReference(new Address($defaultEndpoint)),
211
                ]),
212
            ],
213
        );
214
    }
215
216
217
    /**
218
     * This method builds the md:KeyDescriptor elements, if any
219
     *
220
     * @return \SimpleSAML\SAML2\XML\md\KeyDescriptor[]
221
     */
222
    private function getKeyDescriptor(): array
223
    {
224
        $keyDescriptor = [];
225
226
        $keys = $this->metadata->getPublicKeys();
227
        foreach ($keys as $key) {
228
            if ($key['type'] !== 'X509Certificate') {
229
                continue;
230
            }
231
            if (!isset($key['signing']) || $key['signing'] === true) {
232
                $keyDescriptor[] = self::buildKeyDescriptor(
233
                    'signing',
234
                    $key['X509Certificate'],
235
                    $key['name'] ?? null,
236
                );
237
            }
238
            if (!isset($key['encryption']) || $key['encryption'] === true) {
239
                $keyDescriptor[] = self::buildKeyDescriptor(
240
                    'encryption',
241
                    $key['X509Certificate'],
242
                    $key['name'] ?? null,
243
                );
244
            }
245
        }
246
247
        if ($this->metadata->hasValue('https.certData')) {
248
            $keyDescriptor[] = self::buildKeyDescriptor('signing', $this->metadata->getString('https.certData'), null);
249
        }
250
251
        return $keyDescriptor;
252
    }
253
254
255
    /**
256
     * This method builds the md:ContactPerson elements, if any
257
     *
258
     * @return \SimpleSAML\SAML2\XML\md\ContactPerson[]
259
     */
260
    private function getContactPerson(): array
261
    {
262
        $contacts = [];
263
264
        foreach ($this->metadata->getOptionalArray('contacts', []) as $contact) {
265
            if (array_key_exists('ContactType', $contact) && array_key_exists('EmailAddress', $contact)) {
266
                $contacts[] = ContactPerson::fromArray($contact);
267
            }
268
        }
269
270
        return $contacts;
271
    }
272
273
274
    /**
275
     * This method builds the md:Extensions, if any
276
     *
277
     * @return \SimpleSAML\SAML2\XML\md\Extensions|null
278
     */
279
    private function getExtensions(): ?Extensions
0 ignored issues
show
Unused Code introduced by
The method getExtensions() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
280
    {
281
        $extensions = [];
282
283
        if ($this->metadata->hasValue('scope')) {
284
            foreach ($this->metadata->getArray('scope') as $scopetext) {
285
                $isRegexpScope = (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext));
286
                $extensions[] = new Scope($scopetext, $isRegexpScope);
287
            }
288
        }
289
290
        if ($this->metadata->hasValue('EntityAttributes')) {
291
            $attr = [];
292
            foreach ($this->metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
293
                $attrValues = [];
294
                foreach ($attributeValues as $attributeValue) {
295
                    $attrValues[] = new AttributeValue($attributeValue);
296
                }
297
298
                // Attribute names that is not URI is prefixed as this: '{nameformat}name'
299
                if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
300
                    $attr[] = new Attribute(
301
                        name: $matches[2],
302
                        nameFormat: $matches[1] === C::NAMEFORMAT_UNSPECIFIED ? null : $matches[1],
303
                        attributeValue: $attrValues,
304
                    );
305
                } else {
306
                    $attr[] = new Attribute(
307
                        name: $attributeName,
308
                        nameFormat: C::NAMEFORMAT_UNSPECIFIED,
309
                        attributeValue: $attrValues,
310
                    );
311
                }
312
            }
313
314
            $extensions[] = new EntityAttributes($attr);
315
        }
316
317
        if ($this->metadata->hasValue('saml:Extensions')) {
318
            $chunks = $this->metadata->getArray('saml:Extensions');
319
            Assert::allIsInstanceOf($chunks, Chunk::class);
320
            $extensions = array_merge($extensions, $chunks);
321
        }
322
323
        if ($this->metadata->hasValue('RegistrationInfo')) {
324
            try {
325
                $extensions[] = RegistrationInfo::fromArray($this->metadata->getArray('RegistrationInfo'));
326
            } catch (ArrayValidationException $err) {
327
                Logger::error('Metadata: invalid content found in RegistrationInfo: ' . $err->getMessage());
328
            }
329
        }
330
331
        if ($this->metadata->hasValue('UIInfo')) {
332
            try {
333
                $extensions[] = UIInfo::fromArray($this->metadata->getArray('UIInfo'));
334
            } catch (ArrayValidationException $err) {
335
                Logger::error('Metadata: invalid content found in UIInfo: ' . $err->getMessage());
336
            }
337
        }
338
339
        if ($this->metadata->hasValue('DiscoHints')) {
340
            try {
341
                $extensions[] = DiscoHints::fromArray($this->metadata->getArray('DiscoHints'));
342
            } catch (ArrayValidationException $err) {
343
                Logger::error('Metadata: invalid content found in DiscoHints: ' . $err->getMessage());
344
            }
345
        }
346
347
        if ($extensions !== []) {
348
            return new Extensions($extensions);
349
        }
350
351
        return null;
352
    }
353
354
355
    /**
356
     * @param string $use
357
     * @param string $x509Cert
358
     * @param string|null $keyName
359
     *
360
     * @return \SimpleSAML\SAML2\XML\md\KeyDescriptor
361
     */
362
    private static function buildKeyDescriptor(string $use, string $x509Cert, ?string $keyName): KeyDescriptor
363
    {
364
        Assert::oneOf($use, ['encryption', 'signing']);
365
        $info = [
366
            new X509Data([
367
                new X509Certificate($x509Cert),
368
            ]),
369
        ];
370
371
        if ($keyName !== null) {
372
            $info[] = new KeyName($keyName);
373
        }
374
375
        return new KeyDescriptor(
376
            new KeyInfo($info),
377
            $use,
378
        );
379
    }
380
}
381