Skip to content

Instantly share code, notes, and snippets.

@ryo-utsunomiya
Last active March 24, 2017 04:26
Show Gist options
  • Save ryo-utsunomiya/9fd9e66e7b1d6e7c6df96daab87fe3db to your computer and use it in GitHub Desktop.
Save ryo-utsunomiya/9fd9e66e7b1d6e7c6df96daab87fe3db to your computer and use it in GitHub Desktop.
Verify an email with PGP/MIME signature
#!/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