|
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 * @subpackage Zend_InfoCard_Xml_Security |
|
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
19 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
20 * @version $Id: Security.php 23280 2010-10-31 10:28:58Z ramon $ |
|
21 */ |
|
22 |
|
23 /** |
|
24 * Zend_InfoCard_Xml_Security_Transform |
|
25 */ |
|
26 require_once 'Zend/InfoCard/Xml/Security/Transform.php'; |
|
27 |
|
28 /** |
|
29 * |
|
30 * @category Zend |
|
31 * @package Zend_InfoCard |
|
32 * @subpackage Zend_InfoCard_Xml_Security |
|
33 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
34 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
35 */ |
|
36 class Zend_InfoCard_Xml_Security |
|
37 { |
|
38 /** |
|
39 * ASN.1 type INTEGER class |
|
40 */ |
|
41 const ASN_TYPE_INTEGER = 0x02; |
|
42 |
|
43 /** |
|
44 * ASN.1 type BIT STRING class |
|
45 */ |
|
46 const ASN_TYPE_BITSTRING = 0x03; |
|
47 |
|
48 /** |
|
49 * ASN.1 type SEQUENCE class |
|
50 */ |
|
51 const ASN_TYPE_SEQUENCE = 0x30; |
|
52 |
|
53 /** |
|
54 * The URI for Canonical Method C14N Exclusive |
|
55 */ |
|
56 const CANONICAL_METHOD_C14N_EXC = 'http://www.w3.org/2001/10/xml-exc-c14n#'; |
|
57 |
|
58 /** |
|
59 * The URI for Signature Method SHA1 |
|
60 */ |
|
61 const SIGNATURE_METHOD_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; |
|
62 |
|
63 /** |
|
64 * The URI for Digest Method SHA1 |
|
65 */ |
|
66 const DIGEST_METHOD_SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; |
|
67 |
|
68 /** |
|
69 * The Identifier for RSA Keys |
|
70 */ |
|
71 const RSA_KEY_IDENTIFIER = '300D06092A864886F70D0101010500'; |
|
72 |
|
73 /** |
|
74 * Constructor (disabled) |
|
75 * |
|
76 * @return void |
|
77 */ |
|
78 private function __construct() |
|
79 { |
|
80 } |
|
81 |
|
82 /** |
|
83 * Validates the signature of a provided XML block |
|
84 * |
|
85 * @param string $strXMLInput An XML block containing a Signature |
|
86 * @return bool True if the signature validated, false otherwise |
|
87 * @throws Zend_InfoCard_Xml_Security_Exception |
|
88 */ |
|
89 static public function validateXMLSignature($strXMLInput) |
|
90 { |
|
91 if(!extension_loaded('openssl')) { |
|
92 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
93 throw new Zend_InfoCard_Xml_Security_Exception("You must have the openssl extension installed to use this class"); |
|
94 } |
|
95 |
|
96 $sxe = simplexml_load_string($strXMLInput); |
|
97 |
|
98 if(!isset($sxe->Signature)) { |
|
99 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
100 throw new Zend_InfoCard_Xml_Security_Exception("Could not identify XML Signature element"); |
|
101 } |
|
102 |
|
103 if(!isset($sxe->Signature->SignedInfo)) { |
|
104 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
105 throw new Zend_InfoCard_Xml_Security_Exception("Signature is missing a SignedInfo block"); |
|
106 } |
|
107 |
|
108 if(!isset($sxe->Signature->SignatureValue)) { |
|
109 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
110 throw new Zend_InfoCard_Xml_Security_Exception("Signature is missing a SignatureValue block"); |
|
111 } |
|
112 |
|
113 if(!isset($sxe->Signature->KeyInfo)) { |
|
114 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
115 throw new Zend_InfoCard_Xml_Security_Exception("Signature is missing a KeyInfo block"); |
|
116 } |
|
117 |
|
118 if(!isset($sxe->Signature->KeyInfo->KeyValue)) { |
|
119 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
120 throw new Zend_InfoCard_Xml_Security_Exception("Signature is missing a KeyValue block"); |
|
121 } |
|
122 |
|
123 switch((string)$sxe->Signature->SignedInfo->CanonicalizationMethod['Algorithm']) { |
|
124 case self::CANONICAL_METHOD_C14N_EXC: |
|
125 $cMethod = (string)$sxe->Signature->SignedInfo->CanonicalizationMethod['Algorithm']; |
|
126 break; |
|
127 default: |
|
128 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
129 throw new Zend_InfoCard_Xml_Security_Exception("Unknown or unsupported CanonicalizationMethod Requested"); |
|
130 break; |
|
131 } |
|
132 |
|
133 switch((string)$sxe->Signature->SignedInfo->SignatureMethod['Algorithm']) { |
|
134 case self::SIGNATURE_METHOD_SHA1: |
|
135 $sMethod = (string)$sxe->Signature->SignedInfo->SignatureMethod['Algorithm']; |
|
136 break; |
|
137 default: |
|
138 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
139 throw new Zend_InfoCard_Xml_Security_Exception("Unknown or unsupported SignatureMethod Requested"); |
|
140 break; |
|
141 } |
|
142 |
|
143 switch((string)$sxe->Signature->SignedInfo->Reference->DigestMethod['Algorithm']) { |
|
144 case self::DIGEST_METHOD_SHA1: |
|
145 $dMethod = (string)$sxe->Signature->SignedInfo->Reference->DigestMethod['Algorithm']; |
|
146 break; |
|
147 default: |
|
148 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
149 throw new Zend_InfoCard_Xml_Security_Exception("Unknown or unsupported DigestMethod Requested"); |
|
150 break; |
|
151 } |
|
152 |
|
153 $base64DecodeSupportsStrictParam = version_compare(PHP_VERSION, '5.2.0', '>='); |
|
154 |
|
155 if ($base64DecodeSupportsStrictParam) { |
|
156 $dValue = base64_decode((string)$sxe->Signature->SignedInfo->Reference->DigestValue, true); |
|
157 } else { |
|
158 $dValue = base64_decode((string)$sxe->Signature->SignedInfo->Reference->DigestValue); |
|
159 } |
|
160 |
|
161 if ($base64DecodeSupportsStrictParam) { |
|
162 $signatureValue = base64_decode((string)$sxe->Signature->SignatureValue, true); |
|
163 } else { |
|
164 $signatureValue = base64_decode((string)$sxe->Signature->SignatureValue); |
|
165 } |
|
166 |
|
167 $transformer = new Zend_InfoCard_Xml_Security_Transform(); |
|
168 |
|
169 foreach($sxe->Signature->SignedInfo->Reference->Transforms->children() as $transform) { |
|
170 $transformer->addTransform((string)$transform['Algorithm']); |
|
171 } |
|
172 |
|
173 $transformed_xml = $transformer->applyTransforms($strXMLInput); |
|
174 |
|
175 $transformed_xml_binhash = pack("H*", sha1($transformed_xml)); |
|
176 |
|
177 if(!self::_secureStringCompare($transformed_xml_binhash, $dValue)) { |
|
178 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
179 throw new Zend_InfoCard_Xml_Security_Exception("Locally Transformed XML does not match XML Document. Cannot Verify Signature"); |
|
180 } |
|
181 |
|
182 $public_key = null; |
|
183 |
|
184 switch(true) { |
|
185 case isset($sxe->Signature->KeyInfo->KeyValue->X509Certificate): |
|
186 |
|
187 $certificate = (string)$sxe->Signature->KeyInfo->KeyValue->X509Certificate; |
|
188 |
|
189 |
|
190 $pem = "-----BEGIN CERTIFICATE-----\n" . |
|
191 wordwrap($certificate, 64, "\n", true) . |
|
192 "\n-----END CERTIFICATE-----"; |
|
193 |
|
194 $public_key = openssl_pkey_get_public($pem); |
|
195 |
|
196 if(!$public_key) { |
|
197 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
198 throw new Zend_InfoCard_Xml_Security_Exception("Unable to extract and prcoess X509 Certificate from KeyValue"); |
|
199 } |
|
200 |
|
201 break; |
|
202 case isset($sxe->Signature->KeyInfo->KeyValue->RSAKeyValue): |
|
203 |
|
204 if(!isset($sxe->Signature->KeyInfo->KeyValue->RSAKeyValue->Modulus) || |
|
205 !isset($sxe->Signature->KeyInfo->KeyValue->RSAKeyValue->Exponent)) { |
|
206 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
207 throw new Zend_InfoCard_Xml_Security_Exception("RSA Key Value not in Modulus/Exponent form"); |
|
208 } |
|
209 |
|
210 $modulus = base64_decode((string)$sxe->Signature->KeyInfo->KeyValue->RSAKeyValue->Modulus); |
|
211 $exponent = base64_decode((string)$sxe->Signature->KeyInfo->KeyValue->RSAKeyValue->Exponent); |
|
212 |
|
213 $pem_public_key = self::_getPublicKeyFromModExp($modulus, $exponent); |
|
214 |
|
215 $public_key = openssl_pkey_get_public ($pem_public_key); |
|
216 |
|
217 break; |
|
218 default: |
|
219 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
220 throw new Zend_InfoCard_Xml_Security_Exception("Unable to determine or unsupported representation of the KeyValue block"); |
|
221 } |
|
222 |
|
223 $transformer = new Zend_InfoCard_Xml_Security_Transform(); |
|
224 $transformer->addTransform((string)$sxe->Signature->SignedInfo->CanonicalizationMethod['Algorithm']); |
|
225 |
|
226 // The way we are doing our XML processing requires that we specifically add this |
|
227 // (even though it's in the <Signature> parent-block).. otherwise, our canonical form |
|
228 // fails signature verification |
|
229 $sxe->Signature->SignedInfo->addAttribute('xmlns', 'http://www.w3.org/2000/09/xmldsig#'); |
|
230 |
|
231 $canonical_signedinfo = $transformer->applyTransforms($sxe->Signature->SignedInfo->asXML()); |
|
232 |
|
233 if(@openssl_verify($canonical_signedinfo, $signatureValue, $public_key)) { |
|
234 return (string)$sxe->Signature->SignedInfo->Reference['URI']; |
|
235 } |
|
236 |
|
237 return false; |
|
238 } |
|
239 |
|
240 /** |
|
241 * Transform an RSA Key in Modulus/Exponent format into a PEM encoding and |
|
242 * return an openssl resource for it |
|
243 * |
|
244 * @param string $modulus The RSA Modulus in binary format |
|
245 * @param string $exponent The RSA exponent in binary format |
|
246 * @return string The PEM encoded version of the key |
|
247 */ |
|
248 static protected function _getPublicKeyFromModExp($modulus, $exponent) |
|
249 { |
|
250 $modulusInteger = self::_encodeValue($modulus, self::ASN_TYPE_INTEGER); |
|
251 $exponentInteger = self::_encodeValue($exponent, self::ASN_TYPE_INTEGER); |
|
252 $modExpSequence = self::_encodeValue($modulusInteger . $exponentInteger, self::ASN_TYPE_SEQUENCE); |
|
253 $modExpBitString = self::_encodeValue($modExpSequence, self::ASN_TYPE_BITSTRING); |
|
254 |
|
255 $binRsaKeyIdentifier = pack( "H*", self::RSA_KEY_IDENTIFIER ); |
|
256 |
|
257 $publicKeySequence = self::_encodeValue($binRsaKeyIdentifier . $modExpBitString, self::ASN_TYPE_SEQUENCE); |
|
258 |
|
259 $publicKeyInfoBase64 = base64_encode( $publicKeySequence ); |
|
260 |
|
261 $publicKeyString = "-----BEGIN PUBLIC KEY-----\n"; |
|
262 $publicKeyString .= wordwrap($publicKeyInfoBase64, 64, "\n", true); |
|
263 $publicKeyString .= "\n-----END PUBLIC KEY-----\n"; |
|
264 |
|
265 return $publicKeyString; |
|
266 } |
|
267 |
|
268 /** |
|
269 * Encode a limited set of data types into ASN.1 encoding format |
|
270 * which is used in X.509 certificates |
|
271 * |
|
272 * @param string $data The data to encode |
|
273 * @param const $type The encoding format constant |
|
274 * @return string The encoded value |
|
275 * @throws Zend_InfoCard_Xml_Security_Exception |
|
276 */ |
|
277 static protected function _encodeValue($data, $type) |
|
278 { |
|
279 // Null pad some data when we get it (integer values > 128 and bitstrings) |
|
280 if( (($type == self::ASN_TYPE_INTEGER) && (ord($data) > 0x7f)) || |
|
281 ($type == self::ASN_TYPE_BITSTRING)) { |
|
282 $data = "\0$data"; |
|
283 } |
|
284 |
|
285 $len = strlen($data); |
|
286 |
|
287 // encode the value based on length of the string |
|
288 // I'm fairly confident that this is by no means a complete implementation |
|
289 // but it is enough for our purposes |
|
290 switch(true) { |
|
291 case ($len < 128): |
|
292 return sprintf("%c%c%s", $type, $len, $data); |
|
293 case ($len < 0x0100): |
|
294 return sprintf("%c%c%c%s", $type, 0x81, $len, $data); |
|
295 case ($len < 0x010000): |
|
296 return sprintf("%c%c%c%c%s", $type, 0x82, $len / 0x0100, $len % 0x0100, $data); |
|
297 default: |
|
298 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
299 throw new Zend_InfoCard_Xml_Security_Exception("Could not encode value"); |
|
300 } |
|
301 |
|
302 require_once 'Zend/InfoCard/Xml/Security/Exception.php'; |
|
303 throw new Zend_InfoCard_Xml_Security_Exception("Invalid code path"); |
|
304 } |
|
305 |
|
306 /** |
|
307 * Securely compare two strings for equality while avoided C level memcmp() |
|
308 * optimisations capable of leaking timing information useful to an attacker |
|
309 * attempting to iteratively guess the unknown string (e.g. password) being |
|
310 * compared against. |
|
311 * |
|
312 * @param string $a |
|
313 * @param string $b |
|
314 * @return bool |
|
315 */ |
|
316 static protected function _secureStringCompare($a, $b) |
|
317 { |
|
318 if (strlen($a) !== strlen($b)) { |
|
319 return false; |
|
320 } |
|
321 $result = 0; |
|
322 for ($i = 0; $i < strlen($a); $i++) { |
|
323 $result |= ord($a[$i]) ^ ord($b[$i]); |
|
324 } |
|
325 return $result == 0; |
|
326 } |
|
327 } |