Last active
March 24, 2017 04:26
-
-
Save ryo-utsunomiya/9fd9e66e7b1d6e7c6df96daab87fe3db to your computer and use it in GitHub Desktop.
Verify an email with PGP/MIME signature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env php | |
<?php | |
/** | |
* gpg_verify.php | |
* | |
* This script requires PHP 5.5+. | |
* | |
* @author Ryo Utsunomiya | |
* @license MIT | |
*/ | |
if (is_directly_called()) { | |
// When this script is called by `php gpg_verify.php`, call main(). | |
exit(main($argc, $argv)); | |
} | |
/** | |
* @param int $argc | |
* @param array $argv | |
* @return int 0 = success, 1+ = error | |
*/ | |
function main($argc, $argv) | |
{ | |
if ($argc < 2) { | |
print_error(usage()); | |
return 1; | |
} | |
$file = get_file_from_argv($argv); | |
if (!$file) { | |
print_error("No such file: {$file}\n"); | |
return 1; | |
} | |
if (!is_multipart_signed($file)) { | |
print_error("Message must be multipart/signed(PGP/MIME)\n"); | |
return 1; | |
} | |
$msg_file = tempnam(sys_get_temp_dir(), __FILE__); | |
$sig_file = "{$msg_file}.asc"; | |
file_put_contents($msg_file, read_message($file)); | |
file_put_contents($sig_file, read_signature($file)); | |
system(gpg_verify_command($sig_file, $msg_file)); | |
unlink($msg_file); | |
unlink($sig_file); | |
return 0; | |
} | |
/** | |
* @param string $message | |
* @return void | |
*/ | |
function print_error($message) | |
{ | |
fputs(STDERR, $message); | |
} | |
/** | |
* @return string | |
*/ | |
function usage() | |
{ | |
global $argv; | |
$script_name = $argv[0]; | |
return "Usage: {$script_name} /path/to/mail\n"; | |
} | |
/** | |
* @param array $argv | |
* @return string Valid file path or false on error | |
*/ | |
function get_file_from_argv(array $argv) | |
{ | |
if (!isset($argv[1])) { | |
return false; | |
} | |
$file = $argv[1]; | |
if (!is_file($file)) { | |
return false; | |
} | |
return $file; | |
} | |
/** | |
* @param string $file | |
* @return resource | |
*/ | |
function try_open($file) | |
{ | |
$handle = @fopen($file, 'r'); | |
if (false === $handle) { | |
throw new RuntimeException("Failed to open file: {$file}"); | |
} | |
return $handle; | |
} | |
/** | |
* Iterates over file pointer. | |
* | |
* @param resource|string $fp File pointer or file path | |
* @return Generator | |
*/ | |
function each_line($fp) | |
{ | |
if (!is_resource($fp)) { | |
$fp = try_open($fp); | |
} | |
while (($buf = fgets($fp))) { | |
yield $buf; | |
} | |
} | |
/** | |
* @param string $file | |
* @return array | |
*/ | |
function get_header($file) | |
{ | |
$prevField = ''; | |
$header = []; | |
foreach (each_line($file) as $line) { | |
if (preg_match('/^From /', $line)) continue; | |
if (preg_match('/^\R*$/', $line)) break; | |
if (preg_match('/^([\w-]+): (.*)$/', $line, $matches)) { | |
$header[$matches[1]] = $matches[2]; | |
$prevField = $matches[1]; | |
} else { | |
$header[$prevField] .= $line; | |
} | |
} | |
return $header; | |
} | |
/** | |
* @param array $header | |
* @return string|false string = Content Type header, false = header does not contains Content-Type header | |
*/ | |
function get_content_type(array $header) | |
{ | |
$header_name = 'Content-Type'; | |
if (isset($header[$header_name])) { | |
return $header[$header_name]; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* @param string $file | |
* @return bool | |
*/ | |
function is_multipart_signed($file) | |
{ | |
$content_type = get_content_type(get_header($file)); | |
if (false === $content_type) { | |
return false; | |
} | |
$parts = preg_split('/;\s+/', $content_type); | |
return $parts[0] === 'multipart/signed'; | |
} | |
/** | |
* @param string $file | |
* @return string | |
*/ | |
function read_message($file) | |
{ | |
$header = get_header($file); | |
$content_type = get_content_type($header); | |
$parts = preg_split('/;\s+/', $content_type); | |
if (preg_match('/^boundary="(.*)"/', $parts[3], $matches)) { | |
$boundary = $matches[1]; | |
} else { | |
throw new RuntimeException("Message does not contain any boundary!\n"); | |
} | |
$fp = try_open($file); | |
// Go to the first line of message | |
foreach (each_line($fp) as $line) { | |
if (str_match("--{$boundary}", $line)) break; | |
} | |
// Read the message | |
$message = []; | |
foreach (each_line($fp) as $line) { | |
if (str_match("--{$boundary}", $line)) break; | |
$message[] = $line; | |
} | |
fclose($fp); | |
array_pop($message); // Dump last (empty) line | |
$message = implode('', $message); | |
$message = preg_replace('/(?<!\r)\n/', "\r\n", $message); | |
return $message; | |
} | |
/** | |
* @param string $file | |
* @return string | |
*/ | |
function read_signature($file) | |
{ | |
$sig = ''; | |
$fp = try_open($file); | |
// Go to the first line of signature | |
foreach (each_line($fp) as $line) { | |
if (str_match('-----BEGIN PGP SIGNATURE-----', $line)) { | |
$sig .= $line; | |
break; | |
} | |
} | |
// Read the signature | |
foreach (each_line($fp) as $line) { | |
$sig .= $line; | |
if (str_match('-----END PGP SIGNATURE-----', $line)) break; | |
} | |
fclose($fp); | |
return $sig; | |
} | |
/** | |
* @param string $sig_file | |
* @param string|null $msg_file | |
* @return string | |
*/ | |
function gpg_verify_command($sig_file, $msg_file = null) | |
{ | |
$options = [ | |
'--status-fd=1', // Outputs inner status of gpg | |
'--batch', // Do not use interactive mode | |
'--verify', | |
]; | |
$args = [escapeshellarg($sig_file)]; | |
if (!is_null($msg_file)) { | |
$args[] = escapeshellarg($msg_file); | |
} | |
return 'gpg ' . implode(' ', array_merge($options, $args)); | |
} | |
/** | |
* @param string $part | |
* @param string $string | |
* @return bool | |
*/ | |
function str_match($part, $string) | |
{ | |
return false !== strpos($string, $part); | |
} | |
/** | |
* @return bool | |
*/ | |
function is_directly_called() | |
{ | |
return basename($_SERVER['SCRIPT_NAME']) === basename(__FILE__); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment