DKIMSigner.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <?php
  2. /*
  3. * This file is part of SwiftMailer.
  4. * (c) 2004-2009 Chris Corbyn
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. /**
  10. * DKIM Signer used to apply DKIM Signature to a message.
  11. *
  12. * @author Xavier De Cock <xdecock@gmail.com>
  13. */
  14. class Swift_Signers_DKIMSigner implements Swift_Signers_HeaderSigner
  15. {
  16. /**
  17. * PrivateKey.
  18. *
  19. * @var string
  20. */
  21. protected $privateKey;
  22. /**
  23. * DomainName.
  24. *
  25. * @var string
  26. */
  27. protected $domainName;
  28. /**
  29. * Selector.
  30. *
  31. * @var string
  32. */
  33. protected $selector;
  34. private $passphrase = '';
  35. /**
  36. * Hash algorithm used.
  37. *
  38. * @see RFC6376 3.3: Signers MUST implement and SHOULD sign using rsa-sha256.
  39. *
  40. * @var string
  41. */
  42. protected $hashAlgorithm = 'rsa-sha256';
  43. /**
  44. * Body canon method.
  45. *
  46. * @var string
  47. */
  48. protected $bodyCanon = 'simple';
  49. /**
  50. * Header canon method.
  51. *
  52. * @var string
  53. */
  54. protected $headerCanon = 'simple';
  55. /**
  56. * Headers not being signed.
  57. *
  58. * @var array
  59. */
  60. protected $ignoredHeaders = ['return-path' => true];
  61. /**
  62. * Signer identity.
  63. *
  64. * @var string
  65. */
  66. protected $signerIdentity;
  67. /**
  68. * BodyLength.
  69. *
  70. * @var int
  71. */
  72. protected $bodyLen = 0;
  73. /**
  74. * Maximum signedLen.
  75. *
  76. * @var int
  77. */
  78. protected $maxLen = PHP_INT_MAX;
  79. /**
  80. * Embbed bodyLen in signature.
  81. *
  82. * @var bool
  83. */
  84. protected $showLen = false;
  85. /**
  86. * When the signature has been applied (true means time()), false means not embedded.
  87. *
  88. * @var mixed
  89. */
  90. protected $signatureTimestamp = true;
  91. /**
  92. * When will the signature expires false means not embedded, if sigTimestamp is auto
  93. * Expiration is relative, otherwise it's absolute.
  94. *
  95. * @var int
  96. */
  97. protected $signatureExpiration = false;
  98. /**
  99. * Must we embed signed headers?
  100. *
  101. * @var bool
  102. */
  103. protected $debugHeaders = false;
  104. // work variables
  105. /**
  106. * Headers used to generate hash.
  107. *
  108. * @var array
  109. */
  110. protected $signedHeaders = [];
  111. /**
  112. * If debugHeaders is set store debugData here.
  113. *
  114. * @var string[]
  115. */
  116. private $debugHeadersData = [];
  117. /**
  118. * Stores the bodyHash.
  119. *
  120. * @var string
  121. */
  122. private $bodyHash = '';
  123. /**
  124. * Stores the signature header.
  125. *
  126. * @var Swift_Mime_Headers_ParameterizedHeader
  127. */
  128. protected $dkimHeader;
  129. private $bodyHashHandler;
  130. private $headerHash;
  131. private $headerCanonData = '';
  132. private $bodyCanonEmptyCounter = 0;
  133. private $bodyCanonIgnoreStart = 2;
  134. private $bodyCanonSpace = false;
  135. private $bodyCanonLastChar = null;
  136. private $bodyCanonLine = '';
  137. private $bound = [];
  138. /**
  139. * Constructor.
  140. *
  141. * @param string $privateKey
  142. * @param string $domainName
  143. * @param string $selector
  144. * @param string $passphrase
  145. */
  146. public function __construct($privateKey, $domainName, $selector, $passphrase = '')
  147. {
  148. $this->privateKey = $privateKey;
  149. $this->domainName = $domainName;
  150. $this->signerIdentity = '@'.$domainName;
  151. $this->selector = $selector;
  152. $this->passphrase = $passphrase;
  153. }
  154. /**
  155. * Reset the Signer.
  156. *
  157. * @see Swift_Signer::reset()
  158. */
  159. public function reset()
  160. {
  161. $this->headerHash = null;
  162. $this->signedHeaders = [];
  163. $this->bodyHash = null;
  164. $this->bodyHashHandler = null;
  165. $this->bodyCanonIgnoreStart = 2;
  166. $this->bodyCanonEmptyCounter = 0;
  167. $this->bodyCanonLastChar = null;
  168. $this->bodyCanonSpace = false;
  169. }
  170. /**
  171. * Writes $bytes to the end of the stream.
  172. *
  173. * Writing may not happen immediately if the stream chooses to buffer. If
  174. * you want to write these bytes with immediate effect, call {@link commit()}
  175. * after calling write().
  176. *
  177. * This method returns the sequence ID of the write (i.e. 1 for first, 2 for
  178. * second, etc etc).
  179. *
  180. * @param string $bytes
  181. *
  182. * @return int
  183. *
  184. * @throws Swift_IoException
  185. */
  186. // TODO fix return
  187. public function write($bytes)
  188. {
  189. $this->canonicalizeBody($bytes);
  190. foreach ($this->bound as $is) {
  191. $is->write($bytes);
  192. }
  193. }
  194. /**
  195. * For any bytes that are currently buffered inside the stream, force them
  196. * off the buffer.
  197. */
  198. public function commit()
  199. {
  200. // Nothing to do
  201. return;
  202. }
  203. /**
  204. * Attach $is to this stream.
  205. *
  206. * The stream acts as an observer, receiving all data that is written.
  207. * All {@link write()} and {@link flushBuffers()} operations will be mirrored.
  208. */
  209. public function bind(Swift_InputByteStream $is)
  210. {
  211. // Don't have to mirror anything
  212. $this->bound[] = $is;
  213. return;
  214. }
  215. /**
  216. * Remove an already bound stream.
  217. *
  218. * If $is is not bound, no errors will be raised.
  219. * If the stream currently has any buffered data it will be written to $is
  220. * before unbinding occurs.
  221. */
  222. public function unbind(Swift_InputByteStream $is)
  223. {
  224. // Don't have to mirror anything
  225. foreach ($this->bound as $k => $stream) {
  226. if ($stream === $is) {
  227. unset($this->bound[$k]);
  228. return;
  229. }
  230. }
  231. }
  232. /**
  233. * Flush the contents of the stream (empty it) and set the internal pointer
  234. * to the beginning.
  235. *
  236. * @throws Swift_IoException
  237. */
  238. public function flushBuffers()
  239. {
  240. $this->reset();
  241. }
  242. /**
  243. * Set hash_algorithm, must be one of rsa-sha256 | rsa-sha1.
  244. *
  245. * @param string $hash 'rsa-sha1' or 'rsa-sha256'
  246. *
  247. * @throws Swift_SwiftException
  248. *
  249. * @return $this
  250. */
  251. public function setHashAlgorithm($hash)
  252. {
  253. switch ($hash) {
  254. case 'rsa-sha1':
  255. $this->hashAlgorithm = 'rsa-sha1';
  256. break;
  257. case 'rsa-sha256':
  258. $this->hashAlgorithm = 'rsa-sha256';
  259. if (!\defined('OPENSSL_ALGO_SHA256')) {
  260. throw new Swift_SwiftException('Unable to set sha256 as it is not supported by OpenSSL.');
  261. }
  262. break;
  263. default:
  264. throw new Swift_SwiftException('Unable to set the hash algorithm, must be one of rsa-sha1 or rsa-sha256 (%s given).', $hash);
  265. }
  266. return $this;
  267. }
  268. /**
  269. * Set the body canonicalization algorithm.
  270. *
  271. * @param string $canon
  272. *
  273. * @return $this
  274. */
  275. public function setBodyCanon($canon)
  276. {
  277. if ('relaxed' == $canon) {
  278. $this->bodyCanon = 'relaxed';
  279. } else {
  280. $this->bodyCanon = 'simple';
  281. }
  282. return $this;
  283. }
  284. /**
  285. * Set the header canonicalization algorithm.
  286. *
  287. * @param string $canon
  288. *
  289. * @return $this
  290. */
  291. public function setHeaderCanon($canon)
  292. {
  293. if ('relaxed' == $canon) {
  294. $this->headerCanon = 'relaxed';
  295. } else {
  296. $this->headerCanon = 'simple';
  297. }
  298. return $this;
  299. }
  300. /**
  301. * Set the signer identity.
  302. *
  303. * @param string $identity
  304. *
  305. * @return $this
  306. */
  307. public function setSignerIdentity($identity)
  308. {
  309. $this->signerIdentity = $identity;
  310. return $this;
  311. }
  312. /**
  313. * Set the length of the body to sign.
  314. *
  315. * @param mixed $len (bool or int)
  316. *
  317. * @return $this
  318. */
  319. public function setBodySignedLen($len)
  320. {
  321. if (true === $len) {
  322. $this->showLen = true;
  323. $this->maxLen = PHP_INT_MAX;
  324. } elseif (false === $len) {
  325. $this->showLen = false;
  326. $this->maxLen = PHP_INT_MAX;
  327. } else {
  328. $this->showLen = true;
  329. $this->maxLen = (int) $len;
  330. }
  331. return $this;
  332. }
  333. /**
  334. * Set the signature timestamp.
  335. *
  336. * @param int $time A timestamp
  337. *
  338. * @return $this
  339. */
  340. public function setSignatureTimestamp($time)
  341. {
  342. $this->signatureTimestamp = $time;
  343. return $this;
  344. }
  345. /**
  346. * Set the signature expiration timestamp.
  347. *
  348. * @param int $time A timestamp
  349. *
  350. * @return $this
  351. */
  352. public function setSignatureExpiration($time)
  353. {
  354. $this->signatureExpiration = $time;
  355. return $this;
  356. }
  357. /**
  358. * Enable / disable the DebugHeaders.
  359. *
  360. * @param bool $debug
  361. *
  362. * @return Swift_Signers_DKIMSigner
  363. */
  364. public function setDebugHeaders($debug)
  365. {
  366. $this->debugHeaders = (bool) $debug;
  367. return $this;
  368. }
  369. /**
  370. * Start Body.
  371. */
  372. public function startBody()
  373. {
  374. // Init
  375. switch ($this->hashAlgorithm) {
  376. case 'rsa-sha256':
  377. $this->bodyHashHandler = hash_init('sha256');
  378. break;
  379. case 'rsa-sha1':
  380. $this->bodyHashHandler = hash_init('sha1');
  381. break;
  382. }
  383. $this->bodyCanonLine = '';
  384. }
  385. /**
  386. * End Body.
  387. */
  388. public function endBody()
  389. {
  390. $this->endOfBody();
  391. }
  392. /**
  393. * Returns the list of Headers Tampered by this plugin.
  394. *
  395. * @return array
  396. */
  397. public function getAlteredHeaders()
  398. {
  399. if ($this->debugHeaders) {
  400. return ['DKIM-Signature', 'X-DebugHash'];
  401. } else {
  402. return ['DKIM-Signature'];
  403. }
  404. }
  405. /**
  406. * Adds an ignored Header.
  407. *
  408. * @param string $header_name
  409. *
  410. * @return Swift_Signers_DKIMSigner
  411. */
  412. public function ignoreHeader($header_name)
  413. {
  414. $this->ignoredHeaders[strtolower($header_name ?? '')] = true;
  415. return $this;
  416. }
  417. /**
  418. * Set the headers to sign.
  419. *
  420. * @return Swift_Signers_DKIMSigner
  421. */
  422. public function setHeaders(Swift_Mime_SimpleHeaderSet $headers)
  423. {
  424. $this->headerCanonData = '';
  425. // Loop through Headers
  426. $listHeaders = $headers->listAll();
  427. foreach ($listHeaders as $hName) {
  428. // Check if we need to ignore Header
  429. if (!isset($this->ignoredHeaders[strtolower($hName ?? '')])) {
  430. if ($headers->has($hName)) {
  431. $tmp = $headers->getAll($hName);
  432. foreach ($tmp as $header) {
  433. if ('' != $header->getFieldBody()) {
  434. $this->addHeader($header->toString());
  435. $this->signedHeaders[] = $header->getFieldName();
  436. }
  437. }
  438. }
  439. }
  440. }
  441. return $this;
  442. }
  443. /**
  444. * Add the signature to the given Headers.
  445. *
  446. * @return Swift_Signers_DKIMSigner
  447. */
  448. public function addSignature(Swift_Mime_SimpleHeaderSet $headers)
  449. {
  450. // Prepare the DKIM-Signature
  451. $params = ['v' => '1', 'a' => $this->hashAlgorithm, 'bh' => base64_encode($this->bodyHash ?? ''), 'd' => $this->domainName, 'h' => implode(': ', $this->signedHeaders), 'i' => $this->signerIdentity, 's' => $this->selector];
  452. if ('simple' != $this->bodyCanon) {
  453. $params['c'] = $this->headerCanon.'/'.$this->bodyCanon;
  454. } elseif ('simple' != $this->headerCanon) {
  455. $params['c'] = $this->headerCanon;
  456. }
  457. if ($this->showLen) {
  458. $params['l'] = $this->bodyLen;
  459. }
  460. if (true === $this->signatureTimestamp) {
  461. $params['t'] = time();
  462. if (false !== $this->signatureExpiration) {
  463. $params['x'] = $params['t'] + $this->signatureExpiration;
  464. }
  465. } else {
  466. if (false !== $this->signatureTimestamp) {
  467. $params['t'] = $this->signatureTimestamp;
  468. }
  469. if (false !== $this->signatureExpiration) {
  470. $params['x'] = $this->signatureExpiration;
  471. }
  472. }
  473. if ($this->debugHeaders) {
  474. $params['z'] = implode('|', $this->debugHeadersData);
  475. }
  476. $string = '';
  477. foreach ($params as $k => $v) {
  478. $string .= $k.'='.$v.'; ';
  479. }
  480. $string = trim($string);
  481. $headers->addTextHeader('DKIM-Signature', $string);
  482. // Add the last DKIM-Signature
  483. $tmp = $headers->getAll('DKIM-Signature');
  484. $this->dkimHeader = end($tmp);
  485. $this->addHeader(trim($this->dkimHeader->toString() ?? '')."\r\n b=", true);
  486. if ($this->debugHeaders) {
  487. $headers->addTextHeader('X-DebugHash', base64_encode($this->headerHash ?? ''));
  488. }
  489. $this->dkimHeader->setValue($string.' b='.trim(chunk_split(base64_encode($this->getEncryptedHash() ?? ''), 73, ' ')));
  490. return $this;
  491. }
  492. /* Private helpers */
  493. protected function addHeader($header, $is_sig = false)
  494. {
  495. switch ($this->headerCanon) {
  496. case 'relaxed':
  497. // Prepare Header and cascade
  498. $exploded = explode(':', $header, 2);
  499. $name = strtolower(trim($exploded[0]));
  500. $value = str_replace("\r\n", '', $exploded[1]);
  501. $value = preg_replace("/[ \t][ \t]+/", ' ', $value);
  502. $header = $name.':'.trim($value).($is_sig ? '' : "\r\n");
  503. // no break
  504. case 'simple':
  505. // Nothing to do
  506. }
  507. $this->addToHeaderHash($header);
  508. }
  509. protected function canonicalizeBody($string)
  510. {
  511. $len = \strlen($string);
  512. $canon = '';
  513. $method = ('relaxed' == $this->bodyCanon);
  514. for ($i = 0; $i < $len; ++$i) {
  515. if ($this->bodyCanonIgnoreStart > 0) {
  516. --$this->bodyCanonIgnoreStart;
  517. continue;
  518. }
  519. switch ($string[$i]) {
  520. case "\r":
  521. $this->bodyCanonLastChar = "\r";
  522. break;
  523. case "\n":
  524. if ("\r" == $this->bodyCanonLastChar) {
  525. if ($method) {
  526. $this->bodyCanonSpace = false;
  527. }
  528. if ('' == $this->bodyCanonLine) {
  529. ++$this->bodyCanonEmptyCounter;
  530. } else {
  531. $this->bodyCanonLine = '';
  532. $canon .= "\r\n";
  533. }
  534. } else {
  535. // Wooops Error
  536. // todo handle it but should never happen
  537. }
  538. break;
  539. case ' ':
  540. case "\t":
  541. if ($method) {
  542. $this->bodyCanonSpace = true;
  543. break;
  544. }
  545. // no break
  546. default:
  547. if ($this->bodyCanonEmptyCounter > 0) {
  548. $canon .= str_repeat("\r\n", $this->bodyCanonEmptyCounter);
  549. $this->bodyCanonEmptyCounter = 0;
  550. }
  551. if ($this->bodyCanonSpace) {
  552. $this->bodyCanonLine .= ' ';
  553. $canon .= ' ';
  554. $this->bodyCanonSpace = false;
  555. }
  556. $this->bodyCanonLine .= $string[$i];
  557. $canon .= $string[$i];
  558. }
  559. }
  560. $this->addToBodyHash($canon);
  561. }
  562. protected function endOfBody()
  563. {
  564. // Add trailing Line return if last line is non empty
  565. if (\strlen($this->bodyCanonLine) > 0) {
  566. $this->addToBodyHash("\r\n");
  567. }
  568. $this->bodyHash = hash_final($this->bodyHashHandler, true);
  569. }
  570. private function addToBodyHash($string)
  571. {
  572. $len = \strlen($string);
  573. if ($len > ($new_len = ($this->maxLen - $this->bodyLen))) {
  574. $string = substr($string, 0, $new_len);
  575. $len = $new_len;
  576. }
  577. hash_update($this->bodyHashHandler, $string);
  578. $this->bodyLen += $len;
  579. }
  580. private function addToHeaderHash($header)
  581. {
  582. if ($this->debugHeaders) {
  583. $this->debugHeadersData[] = trim($header ?? '');
  584. }
  585. $this->headerCanonData .= $header;
  586. }
  587. /**
  588. * @throws Swift_SwiftException
  589. *
  590. * @return string
  591. */
  592. private function getEncryptedHash()
  593. {
  594. $signature = '';
  595. switch ($this->hashAlgorithm) {
  596. case 'rsa-sha1':
  597. $algorithm = OPENSSL_ALGO_SHA1;
  598. break;
  599. case 'rsa-sha256':
  600. $algorithm = OPENSSL_ALGO_SHA256;
  601. break;
  602. }
  603. $pkeyId = openssl_get_privatekey($this->privateKey, $this->passphrase);
  604. if (!$pkeyId) {
  605. throw new Swift_SwiftException('Unable to load DKIM Private Key ['.openssl_error_string().']');
  606. }
  607. if (openssl_sign($this->headerCanonData, $signature, $pkeyId, $algorithm)) {
  608. return $signature;
  609. }
  610. throw new Swift_SwiftException('Unable to sign DKIM Hash ['.openssl_error_string().']');
  611. }
  612. }