From f1d6b7595030ad282e14b0a391258f218c7b623c Mon Sep 17 00:00:00 2001 From: Alexander Sagen Date: Thu, 25 Apr 2024 23:32:55 +0200 Subject: [PATCH] DkimTagValue: Add new encoding support for DKIM1 and DMARC1 --- composer.json | 2 +- src/encoding/dkim_tag_value.php | 84 ++++++++++++++++++++++++ src/encoding/dkim_tag_value/tag.php | 73 ++++++++++++++++++++ src/encoding/dkim_tag_value/tag_list.php | 44 +++++++++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/encoding/dkim_tag_value.php create mode 100644 src/encoding/dkim_tag_value/tag.php create mode 100644 src/encoding/dkim_tag_value/tag_list.php diff --git a/composer.json b/composer.json index 9b2948e..f05be61 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "alexrsagen/obie", - "version": "1.6.5", + "version": "1.6.6", "type": "framework", "description": "Obie is a simple PHP framework. It aims to provide basic services needed for any web app.", "keywords": ["framework", "php", "http", "template", "view", "router", "routing", "model", "models", "session", "sessions"], diff --git a/src/encoding/dkim_tag_value.php b/src/encoding/dkim_tag_value.php new file mode 100644 index 0000000..22b2933 --- /dev/null +++ b/src/encoding/dkim_tag_value.php @@ -0,0 +1,84 @@ +tags[] = new Tag($tag_name, $tag_value); + } + + if ($strict && !$output->isValid($version)) return null; + return $output; + } + + /** + * Encode a DKIM tag-value list + * + * @param TagList $input + * @param string $version + * @return string + */ + public static function encode(TagList $input, string $version = self::VERSION_DKIM1): string { + $output = ''; + $i = 0; + foreach ($input->tags as $tag) { + if ($i > 0) { + $output .= ';'; + } + $output .= $tag->name; + $output .= '='; + if ($version === self::VERSION_DKIM1 && $tag->name === 'n') { + $value = quoted_printable_encode($tag->value); + for ($j = strlen($value) - 1; $j >= 0; $j--) { + $ord = ord($value[$j]); + if ($ord <= 0x20 || $ord >= 0x7F || $ord === 0x3B) { + $value = substr($value, 0, $j) . '=' . strtoupper(dechex($ord)) . substr($value, $j+1); + } + } + $output .= $value; + } else { + $output .= $tag->value; + } + $i++; + } + return $output; + } +} \ No newline at end of file diff --git a/src/encoding/dkim_tag_value/tag.php b/src/encoding/dkim_tag_value/tag.php new file mode 100644 index 0000000..d9cbb30 --- /dev/null +++ b/src/encoding/dkim_tag_value/tag.php @@ -0,0 +1,73 @@ +name === 'v' && $this->value !== $version) { + Log::warning('DkimTagValue/Tag: unexpected version value'); + return false; + } + + // version-specific tags + if ($version === DkimTagValue::VERSION_DKIM1) { + // tags defined in RFC 6376 (DKIM) + if (in_array($this->name, ['h', 'k']) && preg_match(self::REGEX_HYPHENATED_WORD, $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DKIM1 tag "h=" or "k="'); + return false; + } + if ($this->name === 's' && preg_match(self::REGEX_HYPHENATED_WORDS_OR_ASTERISK, $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DKIM1 tag "s="'); + return false; + } + if ($this->name === 't' && preg_match(self::REGEX_HYPHENATED_WORDS, $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DKIM1 tag "t="'); + return false; + } + if ($this->name === 'p' && !SimpleValidator::isValid($this->value, SimpleValidator::TYPE_BASE64)) { + Log::warning('DkimTagValue/Tag: invalid public key value for DKIM1 tag "p="'); + return false; + } + } elseif ($version === DkimTagValue::VERSION_DMARC1) { + // tags defined in RFC 7489 (DMARC) + if (in_array($this->name, ['adkim', 'aspf']) && preg_match('/^[rs]$/', $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid value for DMARC1 tag "adkim=" or "aspf="'); + return false; + } + if ($this->name === 'fo' && preg_match('/^[01ds:]+$/', $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid value for DMARC1 tag "fo="'); + return false; + } + if (in_array($this->name, ['sp','p']) && preg_match('/^(none|quarantine|reject)$/', $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid policy value for DMARC1 tag "sp=" or "p="'); + return false; + } + if ($this->name === 'pct' && preg_match('/^(100|[1-9][0-9]|[0-9])$/', $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid 0-100 numeric value for DMARC1 tag "pct="'); + return false; + } + if ($this->name === 'rf' && preg_match(self::REGEX_HYPHENATED_WORD, $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DMARC1 tag "rf="'); + return false; + } + if ($this->name === 'ri' && preg_match('/^\d+$/', $this->value) !== 1) { + Log::warning('DkimTagValue/Tag: invalid numeric value for DMARC1 tag "ri="'); + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/encoding/dkim_tag_value/tag_list.php b/src/encoding/dkim_tag_value/tag_list.php new file mode 100644 index 0000000..5a3988a --- /dev/null +++ b/src/encoding/dkim_tag_value/tag_list.php @@ -0,0 +1,44 @@ +tags as $tag) { + if ($tag->name === $name) { + $output[] = $tag; + } + } + if (count($output) === 0) return null; + if (count($output) === 1) return $output[0]; + return $output; + } + + public function isValid(string $version = DkimTagValue::VERSION_DKIM1): bool { + $i = 0; + foreach ($this->tags as $tag) { + // The v= tag MUST be the first tag in the record. + if ($tag->name === 'v' && $i !== 0) return false; + if (!$tag->isValid($version)) return false; + $i++; + } + return true; + } + + public function __toString(): string { + return DkimTagValue::encode($this); + } +} \ No newline at end of file