summaryrefslogtreecommitdiffstats
path: root/lib/SimpleSAML/Metadata/Signer.php
blob: f737b92a63cff646477b3f2afdd491eee3e5ea1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
<?php


/**
 * This class implements a helper function for signing of metadata.
 *
 * @author Olav Morken, UNINETT AS.
 * @package SimpleSAMLphp
 */
class SimpleSAML_Metadata_Signer
{

    /**
     * This functions finds what key & certificate files should be used to sign the metadata
     * for the given entity.
     *
     * @param SimpleSAML_Configuration $config Our SimpleSAML_Configuration instance.
     * @param array                    $entityMetadata The metadata of the entity.
     * @param string                   $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
     *     'Shib 1.3 SP'.
     *
     * @return array An associative array with the keys 'privatekey', 'certificate', and optionally 'privatekey_pass'.
     * @throws Exception If the key and certificate used to sign is unknown.
     */
    private static function findKeyCert($config, $entityMetadata, $type)
    {
        // first we look for metadata.privatekey and metadata.certificate in the metadata
        if (array_key_exists('metadata.sign.privatekey', $entityMetadata)
            || array_key_exists('metadata.sign.certificate', $entityMetadata)
        ) {

            if (!array_key_exists('metadata.sign.privatekey', $entityMetadata)
                || !array_key_exists('metadata.sign.certificate', $entityMetadata)
            ) {

                throw new Exception(
                    'Missing either the "metadata.sign.privatekey" or the'.
                    ' "metadata.sign.certificate" configuration option in the metadata for'.
                    ' the '.$type.' "'.$entityMetadata['entityid'].'". If one of'.
                    ' these options is specified, then the other must also be specified.'
                );
            }

            $ret = array(
                'privatekey'  => $entityMetadata['metadata.sign.privatekey'],
                'certificate' => $entityMetadata['metadata.sign.certificate']
            );

            if (array_key_exists('metadata.sign.privatekey_pass', $entityMetadata)) {
                $ret['privatekey_pass'] = $entityMetadata['metadata.sign.privatekey_pass'];
            }

            return $ret;
        }

        // then we look for default values in the global configuration
        $privatekey = $config->getString('metadata.sign.privatekey', null);
        $certificate = $config->getString('metadata.sign.certificate', null);
        if ($privatekey !== null || $certificate !== null) {
            if ($privatekey === null || $certificate === null) {
                throw new Exception(
                    'Missing either the "metadata.sign.privatekey" or the'.
                    ' "metadata.sign.certificate" configuration option in the global'.
                    ' configuration. If one of these options is specified, then the other'.
                    ' must also be specified.'
                );
            }
            $ret = array('privatekey' => $privatekey, 'certificate' => $certificate);

            $privatekey_pass = $config->getString('metadata.sign.privatekey_pass', null);
            if ($privatekey_pass !== null) {
                $ret['privatekey_pass'] = $privatekey_pass;
            }

            return $ret;
        }

        // as a last resort we attempt to use the privatekey and certificate option from the metadata
        if (array_key_exists('privatekey', $entityMetadata)
            || array_key_exists('certificate', $entityMetadata)
        ) {

            if (!array_key_exists('privatekey', $entityMetadata)
                || !array_key_exists('certificate', $entityMetadata)
            ) {
                throw new Exception(
                    'Both the "privatekey" and the "certificate" option must'.
                    ' be set in the metadata for the '.$type.' "'.
                    $entityMetadata['entityid'].'" before it is possible to sign metadata'.
                    ' from this entity.'
                );
            }

            $ret = array(
                'privatekey'  => $entityMetadata['privatekey'],
                'certificate' => $entityMetadata['certificate']
            );

            if (array_key_exists('privatekey_pass', $entityMetadata)) {
                $ret['privatekey_pass'] = $entityMetadata['privatekey_pass'];
            }

            return $ret;
        }

        throw new Exception(
            'Could not find what key & certificate should be used to sign the metadata'.
            ' for the '.$type.' "'.$entityMetadata['entityid'].'".'
        );
    }


    /**
     * Determine whether metadata signing is enabled for the given metadata.
     *
     * @param SimpleSAML_Configuration $config Our SimpleSAML_Configuration instance.
     * @param array                    $entityMetadata The metadata of the entity.
     * @param string                   $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
     *     'Shib 1.3 SP'.
     *
     * @return boolean True if metadata signing is enabled, false otherwise.
     * @throws Exception If the value of the 'metadata.sign.enable' option is not a boolean.
     */
    private static function isMetadataSigningEnabled($config, $entityMetadata, $type)
    {
        // first check the metadata for the entity
        if (array_key_exists('metadata.sign.enable', $entityMetadata)) {
            if (!is_bool($entityMetadata['metadata.sign.enable'])) {
                throw new Exception(
                    'Invalid value for the "metadata.sign.enable" configuration option for'.
                    ' the '.$type.' "'.$entityMetadata['entityid'].'". This option'.
                    ' should be a boolean.'
                );
            }

            return $entityMetadata['metadata.sign.enable'];
        }

        $enabled = $config->getBoolean('metadata.sign.enable', false);

        return $enabled;
    }


    /**
     * Determine the signature and digest algorithms to use when signing metadata.
     *
     * This method will look for the 'metadata.sign.algorithm' key in the $entityMetadata array, or look for such
     * a configuration option in the $config object.
     *
     * @param SimpleSAML_Configuration $config The global configuration.
     * @param array $entityMetadata An array containing the metadata related to this entity.
     * @param string $type A string describing the type of entity. E.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
     *
     * @return array An array with two keys, 'algorithm' and 'digest', corresponding to the signature and digest
     * algorithms to use, respectively.
     *
     * @throws \SimpleSAML\Error\CriticalConfigurationError
     *
     * @todo change to SHA256 by default.
     */
    private static function getMetadataSigningAlgorithm($config, $entityMetadata, $type)
    {
        // configure the algorithm to use
        if (array_key_exists('metadata.sign.algorithm', $entityMetadata)) {
            if (!is_string($entityMetadata['metadata.sign.algorithm'])) {
                throw new \SimpleSAML\Error\CriticalConfigurationError(
                    "Invalid value for the 'metadata.sign.algorithm' configuration option for the ".$type.
                    "'".$entityMetadata['entityid']."'. This option has restricted values"
                );
            }
            $alg = $entityMetadata['metadata.sign.algorithm'];
        } else {
            $alg = $config->getString('metadata.sign.algorithm', XMLSecurityKey::RSA_SHA1);
        }

        $supported_algs = array(
            XMLSecurityKey::RSA_SHA1,
            XMLSecurityKey::RSA_SHA256,
            XMLSecurityKey::RSA_SHA384,
            XMLSecurityKey::RSA_SHA512,
        );

        if (!in_array($alg, $supported_algs)) {
            throw new \SimpleSAML\Error\CriticalConfigurationError("Unknown signature algorithm '$alg'");
        }

        switch ($alg) {
            case XMLSecurityKey::RSA_SHA256:
                $digest = XMLSecurityDSig::SHA256;
                break;
            case XMLSecurityKey::RSA_SHA384:
                $digest = XMLSecurityDSig::SHA384;
                break;
            case XMLSecurityKey::RSA_SHA512:
                $digest = XMLSecurityDSig::SHA512;
                break;
            default:
                $digest = XMLSecurityDSig::SHA1;
        }

        return array(
            'algorithm' => $alg,
            'digest' => $digest,
        );
    }


    /**
     * Signs the given metadata if metadata signing is enabled.
     *
     * @param string $metadataString A string with the metadata.
     * @param array  $entityMetadata The metadata of the entity.
     * @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
     *
     * @return string The $metadataString with the signature embedded.
     * @throws Exception If the certificate or private key cannot be loaded, or the metadata doesn't parse properly.
     */
    public static function sign($metadataString, $entityMetadata, $type)
    {
        $config = SimpleSAML_Configuration::getInstance();

        // check if metadata signing is enabled
        if (!self::isMetadataSigningEnabled($config, $entityMetadata, $type)) {
            return $metadataString;
        }

        // find the key & certificate which should be used to sign the metadata
        $keyCertFiles = self::findKeyCert($config, $entityMetadata, $type);

        $keyFile = \SimpleSAML\Utils\Config::getCertPath($keyCertFiles['privatekey']);
        if (!file_exists($keyFile)) {
            throw new Exception('Could not find private key file ['.$keyFile.'], which is needed to sign the metadata');
        }
        $keyData = file_get_contents($keyFile);

        $certFile = \SimpleSAML\Utils\Config::getCertPath($keyCertFiles['certificate']);
        if (!file_exists($certFile)) {
            throw new Exception(
                'Could not find certificate file ['.$certFile.'], which is needed to sign the metadata'
            );
        }
        $certData = file_get_contents($certFile);


        // convert the metadata to a DOM tree
        try {
            $xml = SAML2_DOMDocumentFactory::fromString($metadataString);
        } catch(Exception $e) {
            throw new Exception('Error parsing self-generated metadata.');
        }

        $signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type);

        // load the private key
        $objKey = new XMLSecurityKey($signature_cf['algorithm'], array('type' => 'private'));
        if (array_key_exists('privatekey_pass', $keyCertFiles)) {
            $objKey->passphrase = $keyCertFiles['privatekey_pass'];
        }
        $objKey->loadKey($keyData, false);

        // get the EntityDescriptor node we should sign
        $rootNode = $xml->firstChild;

        // sign the metadata with our private key
        if ($type == 'ADFS IdP') {
            $objXMLSecDSig = new sspmod_adfs_XMLSecurityDSig($metadataString);
        } else {
            $objXMLSecDSig = new XMLSecurityDSig();
        }

        $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);

        $objXMLSecDSig->addReferenceList(
            array($rootNode),
            $signature_cf['digest'],
            array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
            array('id_name' => 'ID')
        );

        $objXMLSecDSig->sign($objKey);

        // add the certificate to the signature
        $objXMLSecDSig->add509Cert($certData, true);

        // add the signature to the metadata
        $objXMLSecDSig->insertSignature($rootNode, $rootNode->firstChild);

        // return the DOM tree as a string
        return $xml->saveXML();
    }
}