Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\Mime\Crypto;
13
 
14
use Symfony\Component\Mime\Exception\InvalidArgumentException;
15
use Symfony\Component\Mime\Exception\RuntimeException;
16
use Symfony\Component\Mime\Header\UnstructuredHeader;
17
use Symfony\Component\Mime\Message;
18
use Symfony\Component\Mime\Part\AbstractPart;
19
 
20
/**
21
 * @author Fabien Potencier <fabien@symfony.com>
22
 *
23
 * RFC 6376 and 8301
24
 */
25
final class DkimSigner
26
{
27
    public const CANON_SIMPLE = 'simple';
28
    public const CANON_RELAXED = 'relaxed';
29
 
30
    public const ALGO_SHA256 = 'rsa-sha256';
31
    public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463
32
 
33
    private \OpenSSLAsymmetricKey $key;
34
    private string $domainName;
35
    private string $selector;
36
    private array $defaultOptions;
37
 
38
    /**
39
     * @param string $pk         The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format)
40
     * @param string $passphrase A passphrase of the private key (if any)
41
     */
42
    public function __construct(string $pk, string $domainName, string $selector, array $defaultOptions = [], string $passphrase = '')
43
    {
44
        if (!\extension_loaded('openssl')) {
45
            throw new \LogicException('PHP extension "openssl" is required to use DKIM.');
46
        }
47
        $this->key = openssl_pkey_get_private($pk, $passphrase) ?: throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string());
48
        $this->domainName = $domainName;
49
        $this->selector = $selector;
50
        $this->defaultOptions = $defaultOptions + [
51
            'algorithm' => self::ALGO_SHA256,
52
            'signature_expiration_delay' => 0,
53
            'body_max_length' => \PHP_INT_MAX,
54
            'body_show_length' => false,
55
            'header_canon' => self::CANON_RELAXED,
56
            'body_canon' => self::CANON_RELAXED,
57
            'headers_to_ignore' => [],
58
        ];
59
    }
60
 
61
    public function sign(Message $message, array $options = []): Message
62
    {
63
        $options += $this->defaultOptions;
64
        if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) {
65
            throw new InvalidArgumentException(sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm']));
66
        }
67
        $headersToIgnore['return-path'] = true;
68
        $headersToIgnore['x-transport'] = true;
69
        foreach ($options['headers_to_ignore'] as $name) {
70
            $headersToIgnore[strtolower($name)] = true;
71
        }
72
        unset($headersToIgnore['from']);
73
        $signedHeaderNames = [];
74
        $headerCanonData = '';
75
        $headers = $message->getPreparedHeaders();
76
        foreach ($headers->getNames() as $name) {
77
            foreach ($headers->all($name) as $header) {
78
                if (isset($headersToIgnore[strtolower($header->getName())])) {
79
                    continue;
80
                }
81
 
82
                if ('' !== $header->getBodyAsString()) {
83
                    $headerCanonData .= $this->canonicalizeHeader($header->toString(), $options['header_canon']);
84
                    $signedHeaderNames[] = $header->getName();
85
                }
86
            }
87
        }
88
 
89
        [$bodyHash, $bodyLength] = $this->hashBody($message->getBody(), $options['body_canon'], $options['body_max_length']);
90
 
91
        $params = [
92
            'v' => '1',
93
            'q' => 'dns/txt',
94
            'a' => $options['algorithm'],
95
            'bh' => base64_encode($bodyHash),
96
            'd' => $this->domainName,
97
            'h' => implode(': ', $signedHeaderNames),
98
            'i' => '@'.$this->domainName,
99
            's' => $this->selector,
100
            't' => time(),
101
            'c' => $options['header_canon'].'/'.$options['body_canon'],
102
        ];
103
 
104
        if ($options['body_show_length']) {
105
            $params['l'] = $bodyLength;
106
        }
107
        if ($options['signature_expiration_delay']) {
108
            $params['x'] = $params['t'] + $options['signature_expiration_delay'];
109
        }
110
        $value = '';
111
        foreach ($params as $k => $v) {
112
            $value .= $k.'='.$v.'; ';
113
        }
114
        $value = trim($value);
115
        $header = new UnstructuredHeader('DKIM-Signature', $value);
116
        $headerCanonData .= rtrim($this->canonicalizeHeader($header->toString()."\r\n b=", $options['header_canon']));
117
        if (self::ALGO_SHA256 === $options['algorithm']) {
118
            if (!openssl_sign($headerCanonData, $signature, $this->key, \OPENSSL_ALGO_SHA256)) {
119
                throw new RuntimeException('Unable to sign DKIM hash: '.openssl_error_string());
120
            }
121
        } else {
122
            throw new \RuntimeException(sprintf('The "%s" DKIM signing algorithm is not supported yet.', self::ALGO_ED25519));
123
        }
124
        $header->setValue($value.' b='.trim(chunk_split(base64_encode($signature), 73, ' ')));
125
        $headers->add($header);
126
 
127
        return new Message($headers, $message->getBody());
128
    }
129
 
130
    private function canonicalizeHeader(string $header, string $headerCanon): string
131
    {
132
        if (self::CANON_RELAXED !== $headerCanon) {
133
            return $header."\r\n";
134
        }
135
 
136
        $exploded = explode(':', $header, 2);
137
        $name = strtolower(trim($exploded[0]));
138
        $value = str_replace("\r\n", '', $exploded[1]);
139
        $value = trim(preg_replace("/[ \t][ \t]+/", ' ', $value));
140
 
141
        return $name.':'.$value."\r\n";
142
    }
143
 
144
    private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength): array
145
    {
146
        $hash = hash_init('sha256');
147
        $relaxed = self::CANON_RELAXED === $bodyCanon;
148
        $currentLine = '';
149
        $emptyCounter = 0;
150
        $isSpaceSequence = false;
151
        $length = 0;
152
        foreach ($body->bodyToIterable() as $chunk) {
153
            $canon = '';
154
            for ($i = 0, $len = \strlen($chunk); $i < $len; ++$i) {
155
                switch ($chunk[$i]) {
156
                    case "\r":
157
                        break;
158
                    case "\n":
159
                        // previous char is always \r
160
                        if ($relaxed) {
161
                            $isSpaceSequence = false;
162
                        }
163
                        if ('' === $currentLine) {
164
                            ++$emptyCounter;
165
                        } else {
166
                            $currentLine = '';
167
                            $canon .= "\r\n";
168
                        }
169
                        break;
170
                    case ' ':
171
                    case "\t":
172
                        if ($relaxed) {
173
                            $isSpaceSequence = true;
174
                            break;
175
                        }
176
                        // no break
177
                    default:
178
                        if ($emptyCounter > 0) {
179
                            $canon .= str_repeat("\r\n", $emptyCounter);
180
                            $emptyCounter = 0;
181
                        }
182
                        if ($isSpaceSequence) {
183
                            $currentLine .= ' ';
184
                            $canon .= ' ';
185
                            $isSpaceSequence = false;
186
                        }
187
                        $currentLine .= $chunk[$i];
188
                        $canon .= $chunk[$i];
189
                }
190
            }
191
 
192
            if ($length + \strlen($canon) >= $maxLength) {
193
                $canon = substr($canon, 0, $maxLength - $length);
194
                $length += \strlen($canon);
195
                hash_update($hash, $canon);
196
 
197
                break;
198
            }
199
 
200
            $length += \strlen($canon);
201
            hash_update($hash, $canon);
202
        }
203
 
204
        // Add trailing Line return if last line is non empty
205
        if ('' !== $currentLine) {
206
            hash_update($hash, "\r\n");
207
            $length += \strlen("\r\n");
208
        }
209
 
210
        if (!$relaxed && 0 === $length) {
211
            hash_update($hash, "\r\n");
212
            $length = 2;
213
        }
214
 
215
        return [hash_final($hash, true), $length];
216
    }
217
}