cleanup remove tabs
[tine20] / tine20 / Tinebase / Auth / ModSsl / Certificate / X509.php
1 <?php
2
3 /**
4  * Tine 2.0
5  *
6  * @package     Tinebase
7  * @subpackage  Auth
8  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
9  * @author      Antonio Carlos da Silva <antonio-carlos.silva@serpro.gov.br>
10  * @author      Mario Cesar Kolling <mario.kolling@serpro.gov.br>
11  * @copyright   Copyright (c) 2009-2013 Serpro (http://www.serpro.gov.br)
12  * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
13  * @todo        parse authorityInfoAccess, caissuer and ocsp
14  * @todo        parse authorityKeyIdentifier
15  * @todo        phpdoc of all methods
16  * 
17  */
18
19 class Tinebase_Auth_ModSsl_Certificate_X509
20 {
21     protected $certificate;
22     protected $casfile;
23     protected $crlspath;
24     protected $serialNumber = null;
25     protected $version = null;
26     protected $subject = null;
27     protected $cn = null;
28     protected $issuer = null;
29     protected $issuerCn = null;
30     protected $hash = null;
31     protected $validFrom = null;
32     protected $validTo = null;
33     protected $canSign = false;
34     protected $canEncrypt = false;
35     protected $email = null;
36     protected $ca = false;
37     protected $authorityKeyIdentifier = null;
38     protected $crlDistributionPoints = null;
39     protected $authorityInfoAccess = null;
40     protected $status =  array();
41     
42     public function __construct($certificate)
43     {
44         $config = Tinebase_Config::getInstance()->get('modssl');
45         $this->status = array('isValid' => true,'errors' => array());
46         $this->casfile = $config->casfile;
47         $this->crlspath = $config->crlspath;
48         $this->certificate = $certificate;
49         $c = openssl_x509_parse($certificate);
50         
51         // define certificate properties
52         $this->serialNumber = $c['serialNumber'];
53         $this->version = $c['version'];
54         $this->subject = $c['subject'];
55         $this->cn = $c['subject']['CN'];
56         $this->issuer = $c['issuer'];
57         $this->issuerCn = $c['issuer']['CN'];
58         $this->hash = $c['hash'];
59         $this->validFrom = new Tinebase_DateTime($c['validFrom_time_t']);
60         $this->validTo = new Tinebase_DateTime($c['validTo_time_t']);
61         $this->_parsePurpose($c['purposes']);
62         $this->_parseExtensions($c['extensions']);
63         
64         if(strtolower($this->casfile) != 'skip') {
65             $this->_validityCheck(); // skip validation, we trust the server's result
66         }
67         
68         if(strtolower($this->crlspath) != 'skip') {
69             $this->_testRevoked(); // skip test,
70         }
71     }
72     
73     protected function _parseExtensions($extensions)
74     {
75         foreach ($extensions as $extension => $value) {
76             $matches = array();
77             switch ($extension) {
78                 case 'subjectAltName':
79                     if (preg_match('/email:(\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b)/i', $value, $matches)) {
80                         $this->email = $matches[1];
81                     }
82                     break;
83                 case 'basicConstraints' :
84                     if (preg_match('/\bCA:(FALSE|TRUE)\b/', $value, $matches)) {
85                         $this->ca = $matches[1] == 'TRUE' ? TRUE : FALSE;
86                     }
87                     break;
88                 case 'crlDistributionPoints' :
89                     $lines = explode(chr(0x0A), trim($value));
90                     foreach ($lines as &$line) {
91                         preg_match('/URI:/', $line, $matches);
92                         $line = preg_replace('/URI:/', '', $line);
93                     }
94                     $this->crlDistributionPoints = $lines;
95                     break;
96                 
97 //                case 'authorityKeyIdentifier' :
98 //                    if (preg_match('/\bkeyid:(\b([A-F0-9]{2}:)+[[A-F0-9]{2}]\b)/', $value, $matches))
99 //                    {
100 //                        $tmp = '';
101 //                    }
102 //                    break;
103 //                 // TODO: ocsp
104 //                case 'authorityInfoAccess' :
105 //                    if (preg_match('/\bCA Issuers - URI:(http(?:s)?\:\/\/[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]{2,6}(?:\/?|(?:\/[\w\-]+)*)(?:\/?|\/\w+\.[a-zA-Z]{2,4}(?:\?[\w]+\=[\w\-]+)?)?(?:\&[\w]+\=[\w\-]+)*\b)/', $value, $matches))
106 //                    {
107 //                        $tmp = '';
108 //                    }
109 //                    break;
110             }
111         }
112     }
113     
114     protected function _parsePurpose($purposes)
115     {
116         foreach ($purposes as $purpose) {
117             switch ($purpose[2]) {
118                 case 'smimesign' :
119                     $this->canSign = $purpose[0] == 1 ? true : false;
120                     break;
121                 case 'smimeencrypt' :
122                     $this->canEncrypt = $purpose[0] == 1 ? true : false;
123                     break;
124             }
125         }
126     }
127     
128     protected function _validityCheck() 
129     {
130         if(!is_file($this->casfile)) {
131             $this->status['errors'][] = 'Invalid Certificate .(CA-01)';  //'CAs file not found.';
132             $this->status['isValid'] = false;
133             return;
134         }
135         
136         $temporary_files = array();
137         $certTempFile = self::generateTempFilename($temporary_files, Tinebase_Core::getTempDir());
138         self::writeTo($certTempFile,$this->certificate);
139         
140         // Get serialnumber  by comand line ...
141         $out = array();
142         $w = exec('openssl x509 -inform PEM -in ' . $certTempFile . ' -noout -serial',$out);
143         $aux = explode('serial=',$out[0]);
144         
145         if(isset($aux[1]))  {
146             $this->serialNumber = $aux[1];
147         } else {
148             $this->serialNumber = null;
149         }
150     
151         $out = array();
152         // certificate verify ...
153         $w = exec('openssl verify -CAfile '.$this->casfile.' '.$certTempFile,$out);
154         self::removeTempFiles($temporary_files);
155         $aux = explode(' ',$w);
156         if(isset($aux[1])) {
157             if($aux[1] != 'OK') {
158                 foreach($out as $item) {
159                     $aux = explode(':',$item);
160                     if(isset($aux[1])) {
161                         $this->status['errors'][] = trim($aux[1]);
162                         $this->status['isValid'] = false;
163                     }
164                 }
165                 return;
166             }
167         } else {
168             $this->status['errors'][] = (isset($aux[1]) ? trim($aux[1]) : 'Couldn\'t verify if certificate was revoked.(CD-01)');
169             $this->status['isValid'] = false;
170         }
171        
172     }
173
174     /**
175      *
176      * @return type 
177      */
178     protected function _testRevoked()
179     {
180         if(!is_dir($this->crlspath)) {
181             $this->status['errors'][] = 'Couldn\'t verify if certificate was revoked.(CD-02)';  // CRL path not found.';
182             $this->status['isValid'] = false;
183             return;
184         }
185         
186         if(!isset($this->crlDistributionPoints[0])) {
187             // Haven't found crl at the certificate
188             $this->status['errors'][] = 'Couldn\'t verify if certificate was revoked.(CD-03)';  // Crl file not found;
189             $this->status['isValid'] = false;
190             return;
191         }
192         
193         $aux = explode('/',$this->crlDistributionPoints[0]);
194         $crl = file_get_contents($this->crlspath . '/' . $aux[count($aux)-1],true);
195         $out = array();
196         $w = exec('openssl crl -in ' . $this->crlspath . '/' . $aux[count($aux)-1] . ' -inform DER -noout -text',$out);
197
198         if(strpos($out[5],'        Next Update: ') === false) {
199             $this->status['errors'][] = 'Couldn\'t verify if certificate was revoked.(CD-04)';  // Invalid crl file found.';
200             $this->status['isValid'] = false;
201             return;
202         } else {
203             // - verify expired crl...
204             $a1 = explode(' Update: ',$out[5]);
205             if(time() >= date_timestamp_get(date_create($a1[1]))) {
206                 $this->status['errors'][] = 'Couldn\'t verify if certificate was revoked.(CD-05)';   // Invalid crl file found.';
207                 $this->status['isValid'] = false;
208                 return;
209             }
210         }
211
212         $aux = array_search('    Serial Number: ' . $this->serialNumber, $out);
213         
214         if($aux) {
215             // cert revoked...
216             $this->status['isValid'] = false;
217             $a1 = explode('Date: ',$out[$aux+1]);
218             $this->status['errors'][] = 'REVOKED Certificate at: ' . $a1[1];
219             return;
220         }
221     }
222
223     /**
224      *
225      * @param type $ab
226      * @param type $q
227      * @param type $flag
228      * @return type 
229      */
230     public static function xBase128($ab,$q,$flag)
231     {
232         $abc = $ab;
233         if( $q > 127 ) {
234             $abc = self::xBase128($abc, floor($q / 128), 0 );
235         }
236         
237         $q = $q % 128;
238         if( $flag) {
239             $abc[] = $q;
240         } else {
241             $abc[] = 0x80 | $q;
242         }
243         
244         return $abc;
245     }
246     
247     /**
248      *
249      * @param type $oid
250      * @return type 
251      */
252     public static function oid2Hex($oid)
253     {
254         $abBinary = array();
255         $parts = explode('.',$oid);
256         $n = 0;
257         $b = 0;
258         
259         for($n = 0; $n < count($parts); $n++) {
260             if($n==0) {
261                 $b = 40 * $parts[$n];
262             } elseif($n==1) {
263                 $b +=  $parts[$n];
264                 $abBinary[] = $b;
265             } else {
266                 $abBinary = self::xBase128($abBinary, $parts[$n], 1 );
267             }
268         }
269         
270         $value =chr(0x06) . chr(count($abBinary));
271         foreach($abBinary as $item) {
272             $value .= chr($item);
273         }
274         
275         return $value;
276     }
277     
278     /**
279      * Transform cert from PEM format to DER
280      *
281      * @param string Certificate PEM format
282      * @return string Certificate DER format
283      */
284     static public function pem2Der($pemCertificate)
285     {
286         $aux = explode(chr(0x0A),$pemCertificate);
287         $derCertificate = '';
288         foreach ($aux as $i) {
289             if($i != '') {
290                 if(substr($i, 0, 5) !== '-----') {
291                     $derCertificate .= $i;
292                 }
293             }
294         }
295         
296         return base64_decode($derCertificate);
297     }
298     
299     public function getSerialNumber()
300     {
301         return $this->serialNumber;
302     }
303
304     public function getVersion()
305     {
306         return $this->version;
307     }
308
309     public function getSubject()
310     {
311         return $this->subject;
312     }
313
314     public function getCn()
315     {
316         return $this->cn;
317     }
318
319     public function getIssuer()
320     {
321         return $this->issuer;
322     }
323
324     public function getIssuerCn()
325     {
326         return $this->issuerCn;
327     }
328
329     public function getHash()
330     {
331         return $this->hash;
332     }
333
334     public function getValidFrom()
335     {
336         return $this->validFrom;
337     }
338
339     public function getValidTo()
340     {
341         return $this->validTo;
342     }
343       
344     public function isCanSign()
345     {
346         return $this->canSign;
347     }
348
349     public function isCanEncrypt()
350     {
351         return $this->canEncrypt;
352     }
353
354     public function getEmail()
355     {
356         return $this->email;
357     }
358
359     public function isCA()
360     {
361         return $this->ca;
362     }
363
364     public function isValid()
365     {
366         return $this->status['isValid'];
367     }
368
369     public function getAuthorityKeyIdentifier()
370     {
371         return $this->authorityKeyIdentifier;
372     }
373
374     public function getCrlDistributionPoints()
375     {
376         return $this->crlDistributionPoints;
377     }
378
379     public function getAuthorityInfoAccess()
380     {
381         return $this->authorityInfoAccess;
382     }
383
384     public function getStatusErrors()
385     {
386         return $this->status['errors'];
387     }
388
389     public static function generateTempFilename(&$tab_arqs, $path)
390     {
391
392         $list = array('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z');
393         $N = $list[rand(0,count($list)-1)].date('U').$list[rand(0,count($list)-1)].RAND(12345,9999999999).$list[rand(0,count($list)-1)].$list[rand(0,count($list)-1)].RAND(12345,9999999999).'.tmp';
394         $aux = $path.'/'.$N;
395         array_push($tab_arqs ,$aux);
396         return  $aux;
397     }
398
399     private static function removeTempFiles($tab_arqs)
400     {
401         foreach($tab_arqs as $file ) {
402             if(file_exists($file)) {
403                 unlink($file);
404             }
405         }
406     }
407
408     public static function writeTo($file, $content)
409     {
410         return file_put_contents($file, $content);   
411     }
412 }