Issues (32)

src/IdP/MetadataBuilder.php (3 issues)

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

108
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(/** @scrutinizer ignore-type */ $algo, $key);
Loading history...
109
110
        $keyInfo = null;
111
        if ($certArray !== null) {
112
            $keyInfo = new KeyInfo([
113
                new X509Data([
114
                    new X509Certificate($certArray['certData']),
115
                ]),
116
            ]);
117
        }
118
119
        $document->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
120
        return $document;
121
    }
122
123
124
    /**
125
     * This method builds the md:Organization element, if any
126
     *
127
     * @return \SimpleSAML\SAML2\XML\md\Organization
128
     */
129
    private function getOrganization(): ?Organization
130
    {
131
        if (
132
            !$this->metadata->hasValue('OrganizationName') ||
133
            !$this->metadata->hasValue('OrganizationDisplayName') ||
134
            !$this->metadata->hasValue('OrganizationURL')
135
        ) {
136
            // empty or incomplete organization information
137
            return null;
138
        }
139
140
        $arrayUtils = new Utils\Arrays();
141
        $org = null;
0 ignored issues
show
The assignment to $org is dead and can be removed.
Loading history...
142
143
        try {
144
            $org = Organization::fromArray([
145
                'OrganizationName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationName'), 'en'),
146
                'OrganizationDisplayName' => $arrayUtils->arrayize(
147
                    $this->metadata->getArray('OrganizationDisplayName'),
148
                    'en',
149
                ),
150
                'OrganizationURL' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationURL'), 'en'),
151
            ]);
152
        } catch (ArrayValidationException $e) {
153
            Logger::error('Federation: invalid content found in contact: ' . $e->getMessage());
154
        }
155
156
        return $org;
157
    }
158
159
160
    /**
161
     * This method builds the role descriptor elements
162
     *
163
     * @return \SimpleSAML\SAML2\XML\md\AbstractRoleDescriptor[]
164
     */
165
    private function getRoleDescriptor(): array
166
    {
167
        $descriptors = [];
168
169
        $set = $this->metadata->getString('metadata-set');
170
        switch ($set) {
171
            case 'adfs-idp-hosted':
172
                $descriptors[] = $this->getSecurityTokenService();
173
                break;
174
            default:
175
                throw new Exception('Not implemented');
176
        }
177
178
        return $descriptors;
179
    }
180
181
182
    /**
183
     * This method builds the SecurityTokenService element
184
     *
185
     * @return \SimpleSAML\WSSecurity\XML\fed\SecurityTokenServiceType
186
     */
187
    public function getSecurityTokenService(): SecurityTokenServiceType
188
    {
189
        $defaultEndpoint = Module::getModuleURL('adfs') . '/idp/prp.php';
190
191
        return new SecurityTokenServiceType(
192
            protocolSupportEnumeration: [C::NS_TRUST_200512, C::NS_TRUST_200502, C::NS_FED],
193
            keyDescriptors: $this->getKeyDescriptor(),
194
            tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]),
195
            securityTokenServiceEndpoint: [
196
                new SecurityTokenServiceEndpoint([
197
                    new EndpointReference(new Address($defaultEndpoint)),
198
                ]),
199
            ],
200
            passiveRequestorEndpoint: [
201
                new PassiveRequestorEndpoint([
202
                    new EndpointReference(new Address($defaultEndpoint)),
203
                ]),
204
            ],
205
        );
206
    }
207
208
209
    /**
210
     * This method builds the md:KeyDescriptor elements, if any
211
     *
212
     * @return \SimpleSAML\SAML2\XML\md\KeyDescriptor[]
213
     */
214
    private function getKeyDescriptor(): array
215
    {
216
        $keyDescriptor = [];
217
218
        $keys = $this->metadata->getPublicKeys();
219
        foreach ($keys as $key) {
220
            if ($key['type'] !== 'X509Certificate') {
221
                continue;
222
            }
223
            if (!isset($key['signing']) || $key['signing'] === true) {
224
                $keyDescriptor[] = self::buildKeyDescriptor(
225
                    'signing',
226
                    $key['X509Certificate'],
227
                    $key['name'] ?? null,
228
                );
229
            }
230
            if (!isset($key['encryption']) || $key['encryption'] === true) {
231
                $keyDescriptor[] = self::buildKeyDescriptor(
232
                    'encryption',
233
                    $key['X509Certificate'],
234
                    $key['name'] ?? null,
235
                );
236
            }
237
        }
238
239
        if ($this->metadata->hasValue('https.certData')) {
240
            $keyDescriptor[] = self::buildKeyDescriptor('signing', $this->metadata->getString('https.certData'), null);
241
        }
242
243
        return $keyDescriptor;
244
    }
245
246
247
    /**
248
     * This method builds the md:ContactPerson elements, if any
249
     *
250
     * @return \SimpleSAML\SAML2\XML\md\ContactPerson[]
251
     */
252
    private function getContactPerson(): array
253
    {
254
        $contacts = [];
255
256
        foreach ($this->metadata->getOptionalArray('contacts', []) as $contact) {
257
            if (array_key_exists('ContactType', $contact) && array_key_exists('EmailAddress', $contact)) {
258
                $contacts[] = ContactPerson::fromArray($contact);
259
            }
260
        }
261
262
        return $contacts;
263
    }
264
265
266
    /**
267
     * This method builds the md:Extensions, if any
268
     *
269
     * @return \SimpleSAML\SAML2\XML\md\Extensions|null
270
     */
271
    private function getExtensions(): ?Extensions
0 ignored issues
show
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...
272
    {
273
        $extensions = [];
274
275
        if ($this->metadata->hasValue('scope')) {
276
            foreach ($this->metadata->getArray('scope') as $scopetext) {
277
                $isRegexpScope = (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext));
278
                $extensions[] = new Scope($scopetext, $isRegexpScope);
279
            }
280
        }
281
282
        if ($this->metadata->hasValue('EntityAttributes')) {
283
            $attr = [];
284
            foreach ($this->metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
285
                $attrValues = [];
286
                foreach ($attributeValues as $attributeValue) {
287
                    $attrValues[] = new AttributeValue($attributeValue);
288
                }
289
290
                // Attribute names that is not URI is prefixed as this: '{nameformat}name'
291
                if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
292
                    $attr[] = new Attribute(
293
                        name: $matches[2],
294
                        nameFormat: $matches[1] === C::NAMEFORMAT_UNSPECIFIED ? null : $matches[1],
295
                        attributeValue: $attrValues,
296
                    );
297
                } else {
298
                    $attr[] = new Attribute(
299
                        name: $attributeName,
300
                        nameFormat: C::NAMEFORMAT_UNSPECIFIED,
301
                        attributeValue: $attrValues,
302
                    );
303
                }
304
            }
305
306
            $extensions[] = new EntityAttributes($attr);
307
        }
308
309
        if ($this->metadata->hasValue('saml:Extensions')) {
310
            $chunks = $this->metadata->getArray('saml:Extensions');
311
            Assert::allIsInstanceOf($chunks, Chunk::class);
312
            $extensions = array_merge($extensions, $chunks);
313
        }
314
315
        if ($this->metadata->hasValue('RegistrationInfo')) {
316
            try {
317
                $extensions[] = RegistrationInfo::fromArray($this->metadata->getArray('RegistrationInfo'));
318
            } catch (ArrayValidationException $err) {
319
                Logger::error('Metadata: invalid content found in RegistrationInfo: ' . $err->getMessage());
320
            }
321
        }
322
323
        if ($this->metadata->hasValue('UIInfo')) {
324
            try {
325
                $extensions[] = UIInfo::fromArray($this->metadata->getArray('UIInfo'));
326
            } catch (ArrayValidationException $err) {
327
                Logger::error('Metadata: invalid content found in UIInfo: ' . $err->getMessage());
328
            }
329
        }
330
331
        if ($this->metadata->hasValue('DiscoHints')) {
332
            try {
333
                $extensions[] = DiscoHints::fromArray($this->metadata->getArray('DiscoHints'));
334
            } catch (ArrayValidationException $err) {
335
                Logger::error('Metadata: invalid content found in DiscoHints: ' . $err->getMessage());
336
            }
337
        }
338
339
        if ($extensions !== []) {
340
            return new Extensions($extensions);
341
        }
342
343
        return null;
344
    }
345
346
347
    /**
348
     * @param string $use
349
     * @param string $x509Cert
350
     * @param string|null $keyName
351
     *
352
     * @return \SimpleSAML\SAML2\XML\md\KeyDescriptor
353
     */
354
    private static function buildKeyDescriptor(string $use, string $x509Cert, ?string $keyName): KeyDescriptor
355
    {
356
        Assert::oneOf($use, ['encryption', 'signing']);
357
        $info = [
358
            new X509Data([
359
                new X509Certificate($x509Cert),
360
            ]),
361
        ];
362
363
        if ($keyName !== null) {
364
            $info[] = new KeyName($keyName);
365
        }
366
367
        return new KeyDescriptor(
368
            new KeyInfo($info),
369
            $use,
370
        );
371
    }
372
}
373