-
Notifications
You must be signed in to change notification settings - Fork 701
Expand file tree
/
Copy pathSigner.php
More file actions
291 lines (242 loc) · 9.51 KB
/
Signer.php
File metadata and controls
291 lines (242 loc) · 9.51 KB
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
<?php
/**
* A helper class for signing XML.
*
* This is a helper class for signing XML documents.
*
* @package simplesamlphp/simplesamlphp
*/
declare(strict_types=1);
namespace SimpleSAML\XML;
use DOMComment;
use DOMElement;
use DOMText;
use Exception;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Utils;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\File;
use function array_key_exists;
class Signer
{
/**
* @var string The name of the ID attribute.
*/
private string $idAttrName = '';
/**
* @var \RobRichards\XMLSecLibs\XMLSecurityKey|false The private key (as an XMLSecurityKey).
*/
private XMLSecurityKey|false $privateKey = false;
/**
* @var string The certificate (as text).
*/
private string $certificate = '';
/**
* @var array Extra certificates which should be included in the response.
*/
private array $extraCertificates = [];
/**
* @var \Symfony\Component\Filesystem\Filesystem
*/
private Filesystem $fileSystem;
/**
* Constructor for the metadata signer.
*
* You can pass an list of options as key-value pairs in the array. This allows you to initialize
* a metadata signer in one call.
*
* The following keys are recognized:
* - privatekey The file with the private key, relative to the cert-directory.
* - privatekey_pass The passphrase for the private key.
* - certificate The file with the certificate, relative to the cert-directory.
* - privatekey_array The private key, as an array returned from \SimpleSAML\Utils\Crypto::loadPrivateKey.
* - publickey_array The public key, as an array returned from \SimpleSAML\Utils\Crypto::loadPublicKey.
* - id The name of the ID attribute.
*
* @param array $options Associative array with options for the constructor. Defaults to an empty array.
*/
public function __construct(array $options = [])
{
$this->fileSystem = new Filesystem();
if (array_key_exists('privatekey', $options)) {
$pass = null;
if (array_key_exists('privatekey_pass', $options)) {
$pass = $options['privatekey_pass'];
}
$this->loadPrivateKey($options['privatekey'], $pass);
}
if (array_key_exists('certificate', $options)) {
$this->loadCertificate($options['certificate']);
}
if (array_key_exists('privatekey_array', $options)) {
$this->loadPrivateKeyArray($options['privatekey_array']);
}
if (array_key_exists('publickey_array', $options)) {
$this->loadPublicKeyArray($options['publickey_array']);
}
if (array_key_exists('id', $options)) {
$this->setIDAttribute($options['id']);
}
}
/**
* Set the private key from an array.
*
* This function loads the private key from an array matching what is returned
* by \SimpleSAML\Utils\Crypto::loadPrivateKey(...).
*
* @param array $privatekey The private key.
*/
public function loadPrivateKeyArray(array $privatekey): void
{
Assert::keyExists($privatekey, 'PEM');
$this->privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
if (array_key_exists('password', $privatekey)) {
$this->privateKey->passphrase = $privatekey['password'];
}
$this->privateKey->loadKey($privatekey['PEM'], false);
}
/**
* Set the private key.
*
* Will throw an exception if unable to load the private key.
*
* @param string $location The location which contains the private key
* @param string|null $pass The passphrase on the private key. Pass no value or NULL if the private
* key is unencrypted.
* @param bool $full_path Whether the location found in the configuration contains the
* full path to the private key or not (only relevant to file locations).
* Default to false.
* @throws \Exception
*/
public function loadPrivateKey(string $location, ?string $pass, bool $full_path = false): void
{
$cryptoUtils = new Utils\Crypto();
$keyData = $cryptoUtils->retrieveKey($location, $full_path);
if ($keyData === null) {
throw new Exception('Could not find private key location "' . $location . '".');
}
$privatekey = ['PEM' => $keyData];
if ($pass !== null) {
$privatekey['password'] = $pass;
}
$this->loadPrivateKeyArray($privatekey);
}
/**
* Set the public key / certificate we should include in the signature.
*
* This function loads the public key from an array matching what is returned
* by \SimpleSAML\Utils\Crypto::loadPublicKey(...).
*
* @param array $publickey The public key.
* @throws \Exception
*/
public function loadPublicKeyArray(array $publickey): void
{
if (!array_key_exists('PEM', $publickey)) {
// We have a public key with only a fingerprint
throw new Exception('Tried to add a certificate fingerprint in a signature.');
}
// For now, we only assume that the public key is an X509 certificate
$this->certificate = $publickey['PEM'];
}
/**
* Set the certificate we should include in the signature.
*
* If this function isn't called, no certificate will be included.
* Will throw an exception if unable to load the certificate.
*
* @param string $file The file which contains the certificate. The path is assumed to be relative to
* the cert-directory.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function loadCertificate(string $file, bool $full_path = false): void
{
if (!$full_path) {
$configUtils = new Utils\Config();
$certFile = $configUtils->getCertPath($file);
} else {
$certFile = $file;
}
if (!$this->fileSystem->exists($certFile)) {
throw new Exception('Could not find certificate file "' . $certFile . '".');
}
$file = new File($certFile);
$this->certificate = $file->getContent();
}
/**
* Set the attribute name for the ID value.
*
* @param string $idAttrName The name of the attribute which contains the id.
*/
public function setIDAttribute(string $idAttrName): void
{
$this->idAttrName = $idAttrName;
}
/**
* Add an extra certificate to the certificate chain in the signature.
*
* Extra certificates will be added to the certificate chain in the order they
* are added.
*
* @param string $location The location which contains the certificate
* @param bool $full_path Whether the location found in the configuration contains the
* full path to the private key or not (only relevant to file locations).
* Default to false.
* @throws \Exception
*/
public function addCertificate(string $location, bool $full_path = false): void
{
$cryptoUtils = new Utils\Crypto();
$certData = $cryptoUtils->retrieveCertificate($location, $full_path);
if ($certData === null) {
throw new Exception('Could not find extra certificate location "' . $location . '".');
}
$this->extraCertificates[] = $certData;
}
/**
* Signs the given DOMElement and inserts the signature at the given position.
*
* The private key must be set before calling this function.
*
* @param \DOMElement $node The DOMElement we should generate a signature for.
* @param \DOMElement $insertInto The DOMElement we should insert the signature element into.
* @param \DOMElement|\DOMComment|\DOMText $insertBefore
* The element we should insert the signature element before. Defaults to NULL,
* in which case the signature will be appended to the element specified in $insertInto.
* @throws \Exception
*/
public function sign(
DOMElement $node,
DOMElement $insertInto,
DOMElement|DOMComment|DOMText|null $insertBefore = null,
): void {
$privateKey = $this->privateKey;
if ($privateKey === false) {
throw new Exception('Private key not set.');
}
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$options = [];
if (!empty($this->idAttrName)) {
$options['id_name'] = $this->idAttrName;
}
$objXMLSecDSig->addReferenceList(
[$node],
XMLSecurityDSig::SHA256,
['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N],
$options,
);
$objXMLSecDSig->sign($privateKey);
// Add the certificate to the signature
$objXMLSecDSig->add509Cert($this->certificate, true);
// Add extra certificates
foreach ($this->extraCertificates as $certificate) {
$objXMLSecDSig->add509Cert($certificate, true);
}
$objXMLSecDSig->insertSignature($insertInto, $insertBefore);
}
}