1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
16: class Horde_Crypt_Smime extends Horde_Crypt
17: {
18: 19: 20: 21: 22: 23: 24: 25: 26:
27: public function verifyPassphrase($private_key, $passphrase)
28: {
29: $res = is_null($passphrase)
30: ? openssl_pkey_get_private($private_key)
31: : openssl_pkey_get_private($private_key, $passphrase);
32:
33: return is_resource($res);
34: }
35:
36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46:
47: public function encrypt($text, $params = array())
48: {
49:
50: $this->checkForOpenSSL();
51:
52: if (isset($params['type'])) {
53: if ($params['type'] === 'message') {
54: return $this->_encryptMessage($text, $params);
55: } elseif ($params['type'] === 'signature') {
56: return $this->_encryptSignature($text, $params);
57: }
58: }
59: }
60:
61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71:
72: public function decrypt($text, $params = array())
73: {
74:
75: $this->checkForOpenSSL();
76:
77: if (isset($params['type'])) {
78: if ($params['type'] === 'message') {
79: return $this->_decryptMessage($text, $params);
80: } elseif (($params['type'] === 'signature') ||
81: ($params['type'] === 'detached-signature')) {
82: return $this->_decryptSignature($text, $params);
83: }
84: }
85: }
86:
87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102:
103: public function verify($text, $certs)
104: {
105:
106: $this->checkForOpenSSL();
107:
108:
109: $input = $this->_createTempFile('horde-smime');
110: $output = $this->_createTempFile('horde-smime');
111:
112:
113: file_put_contents($input, $text);
114: unset($text);
115:
116: $root_certs = array();
117: if (!is_array($certs)) {
118: $certs = array($certs);
119: }
120: foreach ($certs as $file) {
121: if (file_exists($file)) {
122: $root_certs[] = $file;
123: }
124: }
125:
126: $ob = new stdClass;
127:
128: if (!empty($root_certs) &&
129: (openssl_pkcs7_verify($input, 0, $output, $root_certs) === true)) {
130:
131: $ob->msg = Horde_Crypt_Translation::t("Message verified successfully.");
132: $ob->verify = true;
133: } else {
134:
135: $result = openssl_pkcs7_verify($input, PKCS7_NOVERIFY, $output);
136:
137: if ($result === -1) {
138: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Verification failed - an unknown error has occurred."));
139: } elseif ($result === false) {
140: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Verification failed - this message may have been tampered with."));
141: }
142:
143: $ob->msg = Horde_Crypt_Translation::t("Message verified successfully but the signer's certificate could not be verified.");
144: $ob->verify = false;
145: }
146:
147: $ob->cert = file_get_contents($output);
148: $ob->email = $this->getEmailFromKey($ob->cert);
149:
150: return $ob;
151: }
152:
153: 154: 155: 156: 157: 158: 159: 160: 161:
162: public function ($data, $sslpath)
163: {
164:
165: $this->checkForOpenSSL();
166:
167:
168: $input = $this->_createTempFile('horde-smime');
169: $output = $this->_createTempFile('horde-smime');
170:
171:
172: file_put_contents($input, $data);
173: unset($data);
174:
175: exec($sslpath . ' smime -verify -noverify -nochain -in ' . $input . ' -out ' . $output);
176:
177: $ret = file_get_contents($output);
178: if ($ret) {
179: return $ret;
180: }
181:
182: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("OpenSSL error: Could not extract data from signed S/MIME part."));
183: }
184:
185: 186: 187: 188: 189: 190: 191: 192: 193: 194:
195: public function signMIMEPart($mime_part, $params)
196: {
197:
198: $message = $this->encrypt($mime_part->toString(array('headers' => true, 'canonical' => true)), $params);
199:
200:
201: $mime_message = Horde_Mime_Part::parseMessage($message, array('forcemime' => true));
202:
203: $smime_sign = $mime_message->getPart('2');
204: $smime_sign->setDescription(Horde_Crypt_Translation::t("S/MIME Cryptographic Signature"));
205: $smime_sign->setTransferEncoding('base64', array('send' => true));
206:
207: $smime_part = new Horde_Mime_Part();
208: $smime_part->setType('multipart/signed');
209: $smime_part->setContents("This is a cryptographically signed message in MIME format.\n");
210: $smime_part->setContentTypeParameter('protocol', 'application/pkcs7-signature');
211:
212: $smime_part->setContentTypeParameter('micalg', 'sha-1');
213: $smime_part->addPart($mime_part);
214: $smime_part->addPart($smime_sign);
215:
216: return $smime_part;
217: }
218:
219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229:
230: public function encryptMIMEPart($mime_part, $params = array())
231: {
232:
233: $message = $this->encrypt($mime_part->toString(array('headers' => true, 'canonical' => true)), $params);
234:
235: $msg = new Horde_Mime_Part();
236: $msg->setCharset($this->_params['email_charset']);
237: $msg->setHeaderCharset('UTF-8');
238: $msg->setDescription(Horde_Crypt_Translation::t("S/MIME Encrypted Message"));
239: $msg->setDisposition('inline');
240: $msg->setType('application/pkcs7-mime');
241: $msg->setContentTypeParameter('smime-type', 'enveloped-data');
242: $msg->setContents(substr($message, strpos($message, "\n\n") + 2), array('encoding' => 'base64'));
243:
244: return $msg;
245: }
246:
247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261:
262: protected function _encryptMessage($text, $params)
263: {
264:
265: if (!isset($params['pubkey'])) {
266: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public S/MIME key is required to encrypt a message."));
267: }
268:
269:
270: $input = $this->_createTempFile('horde-smime');
271: $output = $this->_createTempFile('horde-smime');
272:
273:
274: file_put_contents($input, $text);
275: unset($text);
276:
277:
278: $ciphers = array(
279: OPENSSL_CIPHER_3DES,
280: OPENSSL_CIPHER_DES,
281: OPENSSL_CIPHER_RC2_128,
282: OPENSSL_CIPHER_RC2_64,
283: OPENSSL_CIPHER_RC2_40
284: );
285:
286: foreach ($ciphers as $val) {
287: if (openssl_pkcs7_encrypt($input, $output, $params['pubkey'], array(), 0, $val)) {
288: $result = file_get_contents($output);
289: if (!empty($result)) {
290: return $this->_fixContentType($result, 'encrypt');
291: }
292: }
293: }
294:
295: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not S/MIME encrypt message."));
296: }
297:
298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318:
319: protected function _encryptSignature($text, $params)
320: {
321:
322: if (!isset($params['pubkey']) ||
323: !isset($params['privkey']) ||
324: !array_key_exists('passphrase', $params)) {
325: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public S/MIME key, private S/MIME key, and passphrase are required to sign a message."));
326: }
327:
328:
329: $input = $this->_createTempFile('horde-smime');
330: $output = $this->_createTempFile('horde-smime');
331: $certs = $this->_createTempFile('horde-smime');
332:
333:
334: file_put_contents($input, $text);
335: unset($text);
336:
337:
338: if (!empty($params['certs'])) {
339: file_put_contents($certs, $params['certs']);
340: }
341:
342:
343: $flags = (isset($params['sigtype']) && ($params['sigtype'] == 'cleartext'))
344: ? PKCS7_TEXT
345: : PKCS7_DETACHED;
346:
347: $privkey = (is_null($params['passphrase'])) ? $params['privkey'] : array($params['privkey'], $params['passphrase']);
348:
349: if (empty($params['certs'])) {
350: $res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags);
351: } else {
352: $res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags, $certs);
353: }
354:
355: if (!$res) {
356: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not S/MIME sign message."));
357: }
358:
359: 360:
361: $fp = fopen($output, 'r');
362: stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
363: stream_filter_append($fp, 'horde_eol');
364: $data = stream_get_contents($fp);
365: fclose($fp);
366:
367: return $this->_fixContentType($data, 'signature');
368: }
369:
370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387:
388: protected function _decryptMessage($text, $params)
389: {
390:
391: if (!isset($params['pubkey']) ||
392: !isset($params['privkey']) ||
393: !array_key_exists('passphrase', $params)) {
394: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public S/MIME key, private S/MIME key, and passphrase are required to decrypt a message."));
395: }
396:
397:
398: $input = $this->_createTempFile('horde-smime');
399: $output = $this->_createTempFile('horde-smime');
400:
401:
402: file_put_contents($input, $text);
403: unset($text);
404:
405: $privkey = is_null($params['passphrase'])
406: ? $params['privkey']
407: : array($params['privkey'], $params['passphrase']);
408: if (openssl_pkcs7_decrypt($input, $output, $params['pubkey'], $privkey)) {
409: return file_get_contents($output);
410: }
411:
412: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not decrypt S/MIME data."));
413: }
414:
415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427:
428: public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
429: $encrypt_params = array())
430: {
431: $part = $this->signMIMEPart($mime_part, $sign_params);
432: return $this->encryptMIMEPart($part, $encrypt_params);
433: }
434:
435: 436: 437: 438: 439: 440: 441:
442: public function certToHTML($cert)
443: {
444: $fieldnames = array(
445:
446: 'description' => Horde_Crypt_Translation::t("Description"),
447: 'emailAddress' => Horde_Crypt_Translation::t("Email Address"),
448: 'commonName' => Horde_Crypt_Translation::t("Common Name"),
449: 'organizationName' => Horde_Crypt_Translation::t("Organisation"),
450: 'organizationalUnitName' => Horde_Crypt_Translation::t("Organisational Unit"),
451: 'countryName' => Horde_Crypt_Translation::t("Country"),
452: 'stateOrProvinceName' => Horde_Crypt_Translation::t("State or Province"),
453: 'localityName' => Horde_Crypt_Translation::t("Location"),
454: 'streetAddress' => Horde_Crypt_Translation::t("Street Address"),
455: 'telephoneNumber' => Horde_Crypt_Translation::t("Telephone Number"),
456: 'surname' => Horde_Crypt_Translation::t("Surname"),
457: 'givenName' => Horde_Crypt_Translation::t("Given Name"),
458:
459:
460: 'exendedtKeyUsage' => Horde_Crypt_Translation::t("X509v3 Extended Key Usage"),
461: 'basicConstraints' => Horde_Crypt_Translation::t("X509v3 Basic Constraints"),
462: 'subjectAltName' => Horde_Crypt_Translation::t("X509v3 Subject Alternative Name"),
463: 'subjectKeyIdentifier' => Horde_Crypt_Translation::t("X509v3 Subject Key Identifier"),
464: 'certificatePolicies' => Horde_Crypt_Translation::t("Certificate Policies"),
465: 'crlDistributionPoints' => Horde_Crypt_Translation::t("CRL Distribution Points"),
466: 'keyUsage' => Horde_Crypt_Translation::t("Key Usage")
467: );
468:
469: $details = $this->parseCert($cert);
470:
471: $text = '<pre class="fixed">';
472:
473:
474: $text .= "<strong>" . Horde_Crypt_Translation::t("Certificate Owner") . ":</strong>\n";
475:
476: foreach ($details['subject'] as $key => $value) {
477: $value = $this->_implodeValues($value);
478: $text .= isset($fieldnames[$key])
479: ? sprintf(" %s: %s\n", $fieldnames[$key], $value)
480: : sprintf(" *%s: %s\n", $key, $value);
481: }
482: $text .= "\n";
483:
484:
485: $text .= "<strong>" . Horde_Crypt_Translation::t("Issuer") . ":</strong>\n";
486:
487: foreach ($details['issuer'] as $key => $value) {
488: $value = $this->_implodeValues($value);
489: $text .= isset($fieldnames[$key])
490: ? sprintf(" %s: %s\n", $fieldnames[$key], $value)
491: : sprintf(" *%s: %s\n", $key, $value);
492: }
493: $text .= "\n";
494:
495:
496: $text .= "<strong>" . Horde_Crypt_Translation::t("Validity") . ":</strong>\n" .
497: sprintf(" %s: %s\n", Horde_Crypt_Translation::t("Not Before"), strftime("%x %X", $details['validity']['notbefore']->getTimestamp())) .
498: sprintf(" %s: %s\n", Horde_Crypt_Translation::t("Not After"), strftime("%x %X", $details['validity']['notafter']->getTimestamp())) .
499: "\n";
500:
501:
502: if (!empty($details['extensions'])) {
503: $text .= "<strong>" . Horde_Crypt_Translation::t("X509v3 extensions") . ":</strong>\n";
504:
505: foreach ($details['extensions'] as $key => $value) {
506: $value = $this->_implodeValues($value, 6);
507: $text .= isset($fieldnames[$key])
508: ? sprintf(" %s:\n %s\n", $fieldnames[$key], trim($value))
509: : sprintf(" *%s:\n %s\n", $key, trim($value));
510: }
511:
512: $text .= "\n";
513: }
514:
515:
516: $text .= "<strong>" . Horde_Crypt_Translation::t("Certificate Details") . ":</strong>\n" .
517: sprintf(" %s: %d\n", Horde_Crypt_Translation::t("Version"), $details['version']) .
518: sprintf(" %s: %d\n", Horde_Crypt_Translation::t("Serial Number"), $details['serialNumber']);
519:
520: return $text . "\n</pre>";
521: }
522:
523: 524: 525: 526: 527: 528: 529: 530:
531: protected function _implodeValues($value, $indent = 4)
532: {
533: if (is_array($value)) {
534: $value = "\n" . str_repeat(' ', $indent)
535: . implode("\n" . str_repeat(' ', $indent), $value);
536: }
537: return $value;
538: }
539:
540: 541: 542: 543: 544: 545: 546:
547: public function parseCert($cert)
548: {
549: $data = openssl_x509_parse($cert, false);
550: if (!$data) {
551: throw new Horde_Crypt_Exception(sprintf(Horde_Crypt_Translation::t("Error parsing S/MIME certficate: %s"), openssl_error_string()));
552: }
553:
554: $details = array(
555: 'extensions' => $data['extensions'],
556: 'issuer' => $data['issuer'],
557: 'serialNumber' => $data['serialNumber'],
558: 'subject' => $data['subject'],
559: 'validity' => array(
560: 'notafter' => new DateTime('@' . $data['validTo_time_t']),
561: 'notbefore' => new DateTime('@' . $data['validFrom_time_t'])
562: ),
563: 'version' => $data['version']
564: );
565:
566:
567: $details['certificate'] = $details;
568:
569: $bc_changes = array(
570: 'emailAddress' => 'Email',
571: 'commonName' => 'CommonName',
572: 'organizationName' => 'Organisation',
573: 'organizationalUnitName' => 'OrganisationalUnit',
574: 'countryName' => 'Country',
575: 'stateOrProvinceName' => 'StateOrProvince',
576: 'localityName' => 'Location',
577: 'streetAddress' => 'StreetAddress',
578: 'telephoneNumber' => 'TelephoneNumber',
579: 'surname' => 'Surname',
580: 'givenName' => 'GivenName'
581: );
582: foreach (array('issuer', 'subject') as $val) {
583: foreach (array_keys($details[$val]) as $key) {
584: if (isset($bc_changes[$key])) {
585: $details['certificate'][$val][$bc_changes[$key]] = $details[$val][$key];
586: unset($details['certificate'][$val][$key]);
587: }
588: }
589: }
590:
591: return $details;
592: }
593:
594: 595: 596: 597: 598: 599: 600: 601: 602:
603: protected function _decryptSignature($text, $params)
604: {
605: throw new Horde_Crypt_Exception('_decryptSignature() ' . Horde_Crypt_Translation::t("not yet implemented"));
606: }
607:
608: 609: 610: 611: 612:
613: public function checkForOpenSSL()
614: {
615: if (!Horde_Util::extensionExists('openssl')) {
616: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("The openssl module is required for the Horde_Crypt_Smime:: class."));
617: }
618: }
619:
620: 621: 622: 623: 624: 625: 626: 627:
628: public function getEmailFromKey($key)
629: {
630: $key_info = openssl_x509_parse($key);
631: if (!is_array($key_info)) {
632: return null;
633: }
634:
635: if (isset($key_info['subject'])) {
636: if (isset($key_info['subject']['Email'])) {
637: return $key_info['subject']['Email'];
638: } elseif (isset($key_info['subject']['emailAddress'])) {
639: return $key_info['subject']['emailAddress'];
640: }
641: }
642:
643:
644: if (isset($key_info['extensions']['subjectAltName'])) {
645: $names = preg_split('/\s*,\s*/', $key_info['extensions']['subjectAltName'], -1, PREG_SPLIT_NO_EMPTY);
646: foreach ($names as $name) {
647: if (strpos($name, ':') === false) {
648: continue;
649: }
650: list($kind, $value) = explode(':', $name, 2);
651: if (Horde_String::lower($kind) == 'email') {
652: return $value;
653: }
654: }
655: }
656:
657: return null;
658: }
659:
660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680:
681: public function parsePKCS12Data($pkcs12, $params)
682: {
683:
684: $this->checkForOpenSSL();
685:
686: if (!isset($params['sslpath'])) {
687: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("No path to the OpenSSL binary provided. The OpenSSL binary is necessary to work with PKCS 12 data."));
688: }
689: $sslpath = escapeshellcmd($params['sslpath']);
690:
691:
692: $input = $this->_createTempFile('horde-smime');
693: $output = $this->_createTempFile('horde-smime');
694:
695: $ob = new stdClass;
696:
697:
698: file_put_contents($input, $pkcs12);
699: unset($pkcs12);
700:
701:
702: $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nocerts';
703: if (isset($params['password'])) {
704: $cmdline .= ' -passin stdin';
705: if (!empty($params['newpassword'])) {
706: $cmdline .= ' -passout stdin';
707: } else {
708: $cmdline .= ' -nodes';
709: }
710: } else {
711: $cmdline .= ' -nodes';
712: }
713:
714: if ($fd = popen($cmdline, 'w')) {
715: fwrite($fd, $params['password'] . "\n");
716: if (!empty($params['newpassword'])) {
717: fwrite($fd, $params['newpassword'] . "\n");
718: }
719: pclose($fd);
720: } else {
721: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to smime binary."));
722: }
723:
724: $ob->private = trim(file_get_contents($output));
725: if (empty($ob->private)) {
726: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Password incorrect"));
727: }
728:
729:
730: $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -clcerts';
731: if (isset($params['password'])) {
732: $cmdline .= ' -passin stdin';
733: }
734:
735: if ($fd = popen($cmdline, 'w')) {
736: fwrite($fd, $params['password'] . "\n");
737: pclose($fd);
738: } else {
739: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to smime binary."));
740: }
741:
742: $ob->public = trim(file_get_contents($output));
743:
744:
745: $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -cacerts';
746: if (isset($params['password'])) {
747: $cmdline .= ' -passin stdin';
748: }
749:
750: if ($fd = popen($cmdline, 'w')) {
751: fwrite($fd, $params['password'] . "\n");
752: pclose($fd);
753: } else {
754: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to smime binary."));
755: }
756:
757: $ob->certs = trim(file_get_contents($output));
758:
759: return $ob;
760: }
761:
762: 763: 764: 765: 766: 767: 768: 769: 770:
771: protected function _fixContentType($text, $type)
772: {
773: if ($type == 'message') {
774: $from = 'application/x-pkcs7-mime';
775: $to = 'application/pkcs7-mime';
776: } else {
777: $from = 'application/x-pkcs7-signature';
778: $to = 'application/pkcs7-signature';
779: }
780: return str_replace('Content-Type: ' . $from, 'Content-Type: ' . $to, $text);
781: }
782:
783: }
784: