|
1 <?php |
|
2 /** |
|
3 * Zend Framework |
|
4 * |
|
5 * LICENSE |
|
6 * |
|
7 * This source file is subject to the new BSD license that is bundled |
|
8 * with this package in the file LICENSE.txt. |
|
9 * It is also available through the world-wide-web at this URL: |
|
10 * http://framework.zend.com/license/new-bsd |
|
11 * If you did not receive a copy of the license and are unable to |
|
12 * obtain it through the world-wide-web, please send an email |
|
13 * to license@zend.com so we can send you a copy immediately. |
|
14 * |
|
15 * @category Zend |
|
16 * @package Zend_InfoCard |
|
17 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
18 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
19 * @version $Id: InfoCard.php 20096 2010-01-06 02:05:09Z bkarwin $ |
|
20 */ |
|
21 |
|
22 /** |
|
23 * Zend_InfoCard_Xml_EncryptedData |
|
24 */ |
|
25 require_once 'Zend/InfoCard/Xml/EncryptedData.php'; |
|
26 |
|
27 /** |
|
28 * Zend_InfoCard_Xml_Assertion |
|
29 */ |
|
30 require_once 'Zend/InfoCard/Xml/Assertion.php'; |
|
31 |
|
32 /** |
|
33 * Zend_InfoCard_Cipher |
|
34 */ |
|
35 require_once 'Zend/InfoCard/Cipher.php'; |
|
36 |
|
37 /** |
|
38 * Zend_InfoCard_Xml_Security |
|
39 */ |
|
40 require_once 'Zend/InfoCard/Xml/Security.php'; |
|
41 |
|
42 /** |
|
43 * Zend_InfoCard_Adapter_Interface |
|
44 */ |
|
45 require_once 'Zend/InfoCard/Adapter/Interface.php'; |
|
46 |
|
47 /** |
|
48 * Zend_InfoCard_Claims |
|
49 */ |
|
50 require_once 'Zend/InfoCard/Claims.php'; |
|
51 |
|
52 /** |
|
53 * @category Zend |
|
54 * @package Zend_InfoCard |
|
55 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
56 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
57 */ |
|
58 class Zend_InfoCard |
|
59 { |
|
60 /** |
|
61 * URI for XML Digital Signature SHA1 Digests |
|
62 */ |
|
63 const DIGEST_SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; |
|
64 |
|
65 /** |
|
66 * An array of certificate pair files and optional passwords for them to search |
|
67 * when trying to determine which certificate was used to encrypt the transient key |
|
68 * |
|
69 * @var Array |
|
70 */ |
|
71 protected $_keyPairs; |
|
72 |
|
73 /** |
|
74 * The instance to use to decrypt public-key encrypted data |
|
75 * |
|
76 * @var Zend_InfoCard_Cipher_Pki_Interface |
|
77 */ |
|
78 protected $_pkiCipherObj; |
|
79 |
|
80 /** |
|
81 * The instance to use to decrypt symmetric encrypted data |
|
82 * |
|
83 * @var Zend_InfoCard_Cipher_Symmetric_Interface |
|
84 */ |
|
85 protected $_symCipherObj; |
|
86 |
|
87 /** |
|
88 * The InfoCard Adapter to use for callbacks into the application using the component |
|
89 * such as when storing assertions, etc. |
|
90 * |
|
91 * @var Zend_InfoCard_Adapter_Interface |
|
92 */ |
|
93 protected $_adapter; |
|
94 |
|
95 |
|
96 /** |
|
97 * InfoCard Constructor |
|
98 * |
|
99 * @throws Zend_InfoCard_Exception |
|
100 */ |
|
101 public function __construct() |
|
102 { |
|
103 $this->_keyPairs = array(); |
|
104 |
|
105 if(!extension_loaded('mcrypt')) { |
|
106 require_once 'Zend/InfoCard/Exception.php'; |
|
107 throw new Zend_InfoCard_Exception("Use of the Zend_InfoCard component requires the mcrypt extension to be enabled in PHP"); |
|
108 } |
|
109 |
|
110 if(!extension_loaded('openssl')) { |
|
111 require_once 'Zend/InfoCard/Exception.php'; |
|
112 throw new Zend_InfoCard_Exception("Use of the Zend_InfoCard component requires the openssl extension to be enabled in PHP"); |
|
113 } |
|
114 } |
|
115 |
|
116 /** |
|
117 * Sets the adapter uesd for callbacks into the application using the component, used |
|
118 * when doing things such as storing / retrieving assertions, etc. |
|
119 * |
|
120 * @param Zend_InfoCard_Adapter_Interface $a The Adapter instance |
|
121 * @return Zend_InfoCard The instnace |
|
122 */ |
|
123 public function setAdapter(Zend_InfoCard_Adapter_Interface $a) |
|
124 { |
|
125 $this->_adapter = $a; |
|
126 return $this; |
|
127 } |
|
128 |
|
129 /** |
|
130 * Retrieves the adapter used for callbacks into the application using the component. |
|
131 * If no adapter was set then an instance of Zend_InfoCard_Adapter_Default is used |
|
132 * |
|
133 * @return Zend_InfoCard_Adapter_Interface The Adapter instance |
|
134 */ |
|
135 public function getAdapter() |
|
136 { |
|
137 if($this->_adapter === null) { |
|
138 require_once 'Zend/InfoCard/Adapter/Default.php'; |
|
139 $this->setAdapter(new Zend_InfoCard_Adapter_Default()); |
|
140 } |
|
141 |
|
142 return $this->_adapter; |
|
143 } |
|
144 |
|
145 /** |
|
146 * Gets the Public Key Cipher object used in this instance |
|
147 * |
|
148 * @return Zend_InfoCard_Cipher_Pki_Interface |
|
149 */ |
|
150 public function getPkiCipherObject() |
|
151 { |
|
152 return $this->_pkiCipherObj; |
|
153 } |
|
154 |
|
155 /** |
|
156 * Sets the Public Key Cipher Object used in this instance |
|
157 * |
|
158 * @param Zend_InfoCard_Cipher_Pki_Interface $cipherObj |
|
159 * @return Zend_InfoCard |
|
160 */ |
|
161 public function setPkiCipherObject(Zend_InfoCard_Cipher_Pki_Interface $cipherObj) |
|
162 { |
|
163 $this->_pkiCipherObj = $cipherObj; |
|
164 return $this; |
|
165 } |
|
166 |
|
167 /** |
|
168 * Get the Symmetric Cipher Object used in this instance |
|
169 * |
|
170 * @return Zend_InfoCard_Cipher_Symmetric_Interface |
|
171 */ |
|
172 public function getSymCipherObject() |
|
173 { |
|
174 return $this->_symCipherObj; |
|
175 } |
|
176 |
|
177 /** |
|
178 * Sets the Symmetric Cipher Object used in this instance |
|
179 * |
|
180 * @param Zend_InfoCard_Cipher_Symmetric_Interface $cipherObj |
|
181 * @return Zend_InfoCard |
|
182 */ |
|
183 public function setSymCipherObject($cipherObj) |
|
184 { |
|
185 $this->_symCipherObj = $cipherObj; |
|
186 return $this; |
|
187 } |
|
188 |
|
189 /** |
|
190 * Remove a Certificate Pair by Key ID from the search list |
|
191 * |
|
192 * @throws Zend_InfoCard_Exception |
|
193 * @param string $key_id The Certificate Key ID returned from adding the certificate pair |
|
194 * @return Zend_InfoCard |
|
195 */ |
|
196 public function removeCertificatePair($key_id) |
|
197 { |
|
198 |
|
199 if(!key_exists($key_id, $this->_keyPairs)) { |
|
200 require_once 'Zend/InfoCard/Exception.php'; |
|
201 throw new Zend_InfoCard_Exception("Attempted to remove unknown key id: $key_id"); |
|
202 } |
|
203 |
|
204 unset($this->_keyPairs[$key_id]); |
|
205 return $this; |
|
206 } |
|
207 |
|
208 /** |
|
209 * Add a Certificate Pair to the list of certificates searched by the component |
|
210 * |
|
211 * @throws Zend_InfoCard_Exception |
|
212 * @param string $private_key_file The path to the private key file for the pair |
|
213 * @param string $public_key_file The path to the certificate / public key for the pair |
|
214 * @param string $type (optional) The URI for the type of key pair this is (default RSA with OAEP padding) |
|
215 * @param string $password (optional) The password for the private key file if necessary |
|
216 * @return string A key ID representing this key pair in the component |
|
217 */ |
|
218 public function addCertificatePair($private_key_file, $public_key_file, $type = Zend_InfoCard_Cipher::ENC_RSA_OAEP_MGF1P, $password = null) |
|
219 { |
|
220 if(!file_exists($private_key_file) || |
|
221 !file_exists($public_key_file)) { |
|
222 require_once 'Zend/InfoCard/Exception.php'; |
|
223 throw new Zend_InfoCard_Exception("Could not locate the public and private certificate pair files: $private_key_file, $public_key_file"); |
|
224 } |
|
225 |
|
226 if(!is_readable($private_key_file) || |
|
227 !is_readable($public_key_file)) { |
|
228 require_once 'Zend/InfoCard/Exception.php'; |
|
229 throw new Zend_InfoCard_Exception("Could not read the public and private certificate pair files (check permissions): $private_key_file, $public_key_file"); |
|
230 } |
|
231 |
|
232 $key_id = md5($private_key_file.$public_key_file); |
|
233 |
|
234 if(key_exists($key_id, $this->_keyPairs)) { |
|
235 require_once 'Zend/InfoCard/Exception.php'; |
|
236 throw new Zend_InfoCard_Exception("Attempted to add previously existing certificate pair: $private_key_file, $public_key_file"); |
|
237 } |
|
238 |
|
239 switch($type) { |
|
240 case Zend_InfoCard_Cipher::ENC_RSA: |
|
241 case Zend_InfoCard_Cipher::ENC_RSA_OAEP_MGF1P: |
|
242 $this->_keyPairs[$key_id] = array('private' => $private_key_file, |
|
243 'public' => $public_key_file, |
|
244 'type_uri' => $type); |
|
245 |
|
246 if($password !== null) { |
|
247 $this->_keyPairs[$key_id]['password'] = $password; |
|
248 } else { |
|
249 $this->_keyPairs[$key_id]['password'] = null; |
|
250 } |
|
251 |
|
252 return $key_id; |
|
253 break; |
|
254 default: |
|
255 require_once 'Zend/InfoCard/Exception.php'; |
|
256 throw new Zend_InfoCard_Exception("Invalid Certificate Pair Type specified: $type"); |
|
257 } |
|
258 } |
|
259 |
|
260 /** |
|
261 * Return a Certificate Pair from a key ID |
|
262 * |
|
263 * @throws Zend_InfoCard_Exception |
|
264 * @param string $key_id The Key ID of the certificate pair in the component |
|
265 * @return array An array containing the path to the private/public key files, |
|
266 * the type URI and the password if provided |
|
267 */ |
|
268 public function getCertificatePair($key_id) |
|
269 { |
|
270 if(key_exists($key_id, $this->_keyPairs)) { |
|
271 return $this->_keyPairs[$key_id]; |
|
272 } |
|
273 |
|
274 require_once 'Zend/InfoCard/Exception.php'; |
|
275 throw new Zend_InfoCard_Exception("Invalid Certificate Pair ID provided: $key_id"); |
|
276 } |
|
277 |
|
278 /** |
|
279 * Retrieve the digest of a given public key / certificate using the provided digest |
|
280 * method |
|
281 * |
|
282 * @throws Zend_InfoCard_Exception |
|
283 * @param string $key_id The certificate key id in the component |
|
284 * @param string $digestMethod The URI of the digest method to use (default SHA1) |
|
285 * @return string The digest value in binary format |
|
286 */ |
|
287 protected function _getPublicKeyDigest($key_id, $digestMethod = self::DIGEST_SHA1) |
|
288 { |
|
289 $certificatePair = $this->getCertificatePair($key_id); |
|
290 |
|
291 $temp = file($certificatePair['public']); |
|
292 unset($temp[count($temp)-1]); |
|
293 unset($temp[0]); |
|
294 $certificateData = base64_decode(implode("\n", $temp)); |
|
295 |
|
296 switch($digestMethod) { |
|
297 case self::DIGEST_SHA1: |
|
298 $digest_retval = sha1($certificateData, true); |
|
299 break; |
|
300 default: |
|
301 require_once 'Zend/InfoCard/Exception.php'; |
|
302 throw new Zend_InfoCard_Exception("Invalid Digest Type Provided: $digestMethod"); |
|
303 } |
|
304 |
|
305 return $digest_retval; |
|
306 } |
|
307 |
|
308 /** |
|
309 * Find a certificate pair based on a digest of its public key / certificate file |
|
310 * |
|
311 * @param string $digest The digest value of the public key wanted in binary form |
|
312 * @param string $digestMethod The URI of the digest method used to calculate the digest |
|
313 * @return mixed The Key ID of the matching certificate pair or false if not found |
|
314 */ |
|
315 protected function _findCertifiatePairByDigest($digest, $digestMethod = self::DIGEST_SHA1) |
|
316 { |
|
317 |
|
318 foreach($this->_keyPairs as $key_id => $certificate_data) { |
|
319 |
|
320 $cert_digest = $this->_getPublicKeyDigest($key_id, $digestMethod); |
|
321 |
|
322 if($cert_digest == $digest) { |
|
323 return $key_id; |
|
324 } |
|
325 } |
|
326 |
|
327 return false; |
|
328 } |
|
329 |
|
330 /** |
|
331 * Extracts the Signed Token from an EncryptedData block |
|
332 * |
|
333 * @throws Zend_InfoCard_Exception |
|
334 * @param string $strXmlToken The EncryptedData XML block |
|
335 * @return string The XML of the Signed Token inside of the EncryptedData block |
|
336 */ |
|
337 protected function _extractSignedToken($strXmlToken) |
|
338 { |
|
339 $encryptedData = Zend_InfoCard_Xml_EncryptedData::getInstance($strXmlToken); |
|
340 |
|
341 // Determine the Encryption Method used to encrypt the token |
|
342 |
|
343 switch($encryptedData->getEncryptionMethod()) { |
|
344 case Zend_InfoCard_Cipher::ENC_AES128CBC: |
|
345 case Zend_InfoCard_Cipher::ENC_AES256CBC: |
|
346 break; |
|
347 default: |
|
348 require_once 'Zend/InfoCard/Exception.php'; |
|
349 throw new Zend_InfoCard_Exception("Unknown Encryption Method used in the secure token"); |
|
350 } |
|
351 |
|
352 // Figure out the Key we are using to decrypt the token |
|
353 |
|
354 $keyinfo = $encryptedData->getKeyInfo(); |
|
355 |
|
356 if(!($keyinfo instanceof Zend_InfoCard_Xml_KeyInfo_XmlDSig)) { |
|
357 require_once 'Zend/InfoCard/Exception.php'; |
|
358 throw new Zend_InfoCard_Exception("Expected a XML digital signature KeyInfo, but was not found"); |
|
359 } |
|
360 |
|
361 |
|
362 $encryptedKey = $keyinfo->getEncryptedKey(); |
|
363 |
|
364 switch($encryptedKey->getEncryptionMethod()) { |
|
365 case Zend_InfoCard_Cipher::ENC_RSA: |
|
366 case Zend_InfoCard_Cipher::ENC_RSA_OAEP_MGF1P: |
|
367 break; |
|
368 default: |
|
369 require_once 'Zend/InfoCard/Exception.php'; |
|
370 throw new Zend_InfoCard_Exception("Unknown Key Encryption Method used in secure token"); |
|
371 } |
|
372 |
|
373 $securityTokenRef = $encryptedKey->getKeyInfo()->getSecurityTokenReference(); |
|
374 |
|
375 $key_id = $this->_findCertifiatePairByDigest($securityTokenRef->getKeyReference()); |
|
376 |
|
377 if(!$key_id) { |
|
378 require_once 'Zend/InfoCard/Exception.php'; |
|
379 throw new Zend_InfoCard_Exception("Unable to find key pair used to encrypt symmetric InfoCard Key"); |
|
380 } |
|
381 |
|
382 $certificate_pair = $this->getCertificatePair($key_id); |
|
383 |
|
384 // Santity Check |
|
385 |
|
386 if($certificate_pair['type_uri'] != $encryptedKey->getEncryptionMethod()) { |
|
387 require_once 'Zend/InfoCard/Exception.php'; |
|
388 throw new Zend_InfoCard_Exception("Certificate Pair which matches digest is not of same algorithm type as document, check addCertificate()"); |
|
389 } |
|
390 |
|
391 $PKcipher = Zend_InfoCard_Cipher::getInstanceByURI($encryptedKey->getEncryptionMethod()); |
|
392 |
|
393 $base64DecodeSupportsStrictParam = version_compare(PHP_VERSION, '5.2.0', '>='); |
|
394 |
|
395 if ($base64DecodeSupportsStrictParam) { |
|
396 $keyCipherValueBase64Decoded = base64_decode($encryptedKey->getCipherValue(), true); |
|
397 } else { |
|
398 $keyCipherValueBase64Decoded = base64_decode($encryptedKey->getCipherValue()); |
|
399 } |
|
400 |
|
401 $symmetricKey = $PKcipher->decrypt( |
|
402 $keyCipherValueBase64Decoded, |
|
403 file_get_contents($certificate_pair['private']), |
|
404 $certificate_pair['password'] |
|
405 ); |
|
406 |
|
407 $symCipher = Zend_InfoCard_Cipher::getInstanceByURI($encryptedData->getEncryptionMethod()); |
|
408 |
|
409 if ($base64DecodeSupportsStrictParam) { |
|
410 $dataCipherValueBase64Decoded = base64_decode($encryptedData->getCipherValue(), true); |
|
411 } else { |
|
412 $dataCipherValueBase64Decoded = base64_decode($encryptedData->getCipherValue()); |
|
413 } |
|
414 |
|
415 $signedToken = $symCipher->decrypt($dataCipherValueBase64Decoded, $symmetricKey); |
|
416 |
|
417 return $signedToken; |
|
418 } |
|
419 |
|
420 /** |
|
421 * Process an input Infomation Card EncryptedData block sent from the client, |
|
422 * validate it, and return the claims contained within it on success or an error message on error |
|
423 * |
|
424 * @param string $strXmlToken The XML token sent to the server from the client |
|
425 * @return Zend_Infocard_Claims The Claims object containing the claims, or any errors which occurred |
|
426 */ |
|
427 public function process($strXmlToken) |
|
428 { |
|
429 |
|
430 $retval = new Zend_InfoCard_Claims(); |
|
431 |
|
432 require_once 'Zend/InfoCard/Exception.php'; |
|
433 try { |
|
434 $signedAssertionsXml = $this->_extractSignedToken($strXmlToken); |
|
435 } catch(Zend_InfoCard_Exception $e) { |
|
436 $retval->setError('Failed to extract assertion document'); |
|
437 $retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE); |
|
438 return $retval; |
|
439 } |
|
440 |
|
441 try { |
|
442 $assertions = Zend_InfoCard_Xml_Assertion::getInstance($signedAssertionsXml); |
|
443 } catch(Zend_InfoCard_Exception $e) { |
|
444 $retval->setError('Failure processing assertion document'); |
|
445 $retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE); |
|
446 return $retval; |
|
447 } |
|
448 |
|
449 if(!($assertions instanceof Zend_InfoCard_Xml_Assertion_Interface)) { |
|
450 throw new Zend_InfoCard_Exception("Invalid Assertion Object returned"); |
|
451 } |
|
452 |
|
453 if(!($reference_id = Zend_InfoCard_Xml_Security::validateXMLSignature($assertions->asXML()))) { |
|
454 $retval->setError("Failure Validating the Signature of the assertion document"); |
|
455 $retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE); |
|
456 return $retval; |
|
457 } |
|
458 |
|
459 // The reference id should be locally scoped as far as I know |
|
460 if($reference_id[0] == '#') { |
|
461 $reference_id = substr($reference_id, 1); |
|
462 } else { |
|
463 $retval->setError("Reference of document signature does not reference the local document"); |
|
464 $retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE); |
|
465 return $retval; |
|
466 } |
|
467 |
|
468 // Make sure the signature is in reference to the same document as the assertions |
|
469 if($reference_id != $assertions->getAssertionID()) { |
|
470 $retval->setError("Reference of document signature does not reference the local document"); |
|
471 $retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE); |
|
472 } |
|
473 |
|
474 // Validate we haven't seen this before and the conditions are acceptable |
|
475 $conditions = $this->getAdapter()->retrieveAssertion($assertions->getAssertionURI(), $assertions->getAssertionID()); |
|
476 |
|
477 if($conditions === false) { |
|
478 $conditions = $assertions->getConditions(); |
|
479 } |
|
480 |
|
481 |
|
482 if(is_array($condition_error = $assertions->validateConditions($conditions))) { |
|
483 $retval->setError("Conditions of assertion document are not met: {$condition_error[1]} ({$condition_error[0]})"); |
|
484 $retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE); |
|
485 } |
|
486 |
|
487 $attributes = $assertions->getAttributes(); |
|
488 |
|
489 $retval->setClaims($attributes); |
|
490 |
|
491 if($retval->getCode() == 0) { |
|
492 $retval->setCode(Zend_InfoCard_Claims::RESULT_SUCCESS); |
|
493 } |
|
494 |
|
495 return $retval; |
|
496 } |
|
497 } |