/library/Zend/InfoCard.php
PHP | 497 lines | 246 code | 82 blank | 169 comment | 31 complexity | cac8620f4d77180ee532aff6194de509 MD5 | raw file
Possible License(s): AGPL-1.0
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-2012 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 24594 2012-01-05 21:27:01Z matthew $
20 */
21
22/**
23 * Zend_InfoCard_Xml_EncryptedData
24 */
25require_once 'Zend/InfoCard/Xml/EncryptedData.php';
26
27/**
28 * Zend_InfoCard_Xml_Assertion
29 */
30require_once 'Zend/InfoCard/Xml/Assertion.php';
31
32/**
33 * Zend_InfoCard_Cipher
34 */
35require_once 'Zend/InfoCard/Cipher.php';
36
37/**
38 * Zend_InfoCard_Xml_Security
39 */
40require_once 'Zend/InfoCard/Xml/Security.php';
41
42/**
43 * Zend_InfoCard_Adapter_Interface
44 */
45require_once 'Zend/InfoCard/Adapter/Interface.php';
46
47/**
48 * Zend_InfoCard_Claims
49 */
50require_once 'Zend/InfoCard/Claims.php';
51
52/**
53 * @category Zend
54 * @package Zend_InfoCard
55 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
56 * @license http://framework.zend.com/license/new-bsd New BSD License
57 */
58class 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}