From 3a39cb6635f490ec25c14b6149528b742ed3c9cb Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Mon, 14 Jul 2025 20:03:44 +0200 Subject: [PATCH] organic: Add pdf signature check --- www/organic/pdf.php | 174 ++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 85 deletions(-) diff --git a/www/organic/pdf.php b/www/organic/pdf.php index 487251f..870842d 100644 --- a/www/organic/pdf.php +++ b/www/organic/pdf.php @@ -40,73 +40,47 @@ function jenc($data): string { return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } +$file = tmpfile(); +$headerfile = tmpfile(); +if (!$file || !$headerfile) { + header('Status: 500'); + header('Content-Length: 0'); + exit; +} +$filename = stream_get_meta_data($file)['uri']; +$headerfilename = stream_get_meta_data($headerfile)['uri']; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $stdin = fopen("php://input", "rb"); + while (!feof($stdin)) { + if (($data = fread($stdin, 8192)) === false) + break; + fwrite($file, $data); + } + fclose($stdin); +} else { + if (exec("curl -s -D " . escapeshellarg($headerfilename) . " -o " . escapeshellarg($filename) . " " . escapeshellarg($url)) === false) { + header('Status: 500'); + header('Content-Length: 0'); + exit; + } +} + if ($format === 'text') { header('Content-Type: text/plain; charset=UTF-8'); - - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $fd_spec = [ - 0 => ["pipe", "r"], // stdin - 1 => ["pipe", "w"], // stdout - 2 => ["pipe", "w"], // stderr - ]; - $process = proc_open(['pdftotext', '-raw', '-', '-'], $fd_spec, $pipes); - $input = fopen("php://input", "rb"); - while (!feof($input)) { - if (($buffer = fread($input, 8192)) === false) - break; - fwrite($pipes[0], $buffer); - } - fclose($input); - fclose($pipes[0]); - - fpassthru($pipes[1]); - fclose($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[2]); - $return_value = proc_close($process); - } else { - passthru("curl -s '" . escapeshellarg($url) . "' | pdftotext -raw - -"); - } + passthru("pdftotext -raw " . escapeshellarg($filename) . " -"); +} else if ($format === 'sig') { + header('Content-Type: text/plain; charset=UTF-8'); + passthru("pdfsig " . escapeshellarg($filename)); } else if ($format === 'json') { header('Content-Type: application/json; charset=UTF-8'); - - $fd_spec = [ - 0 => ["pipe", "r"], // stdin - 1 => ["pipe", "w"], // stdout - 2 => ["pipe", "w"], // stderr - ]; - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $process = proc_open(['pdftotext', '-raw', '-', '-'], $fd_spec, $pipes); - $input = fopen("php://input", "rb"); - while (!feof($input)) { - if (($buffer = fread($input, 8192)) === false) - break; - fwrite($pipes[0], $buffer); - } - fclose($input); - } else { - $process = proc_open( - ['bash', '-c', - "curl -s " . escapeshellarg($url) . " | " . - "pdftotext -raw - -"], - $fd_spec, - $pipes - ); - } - - fclose($pipes[0]); - $text = stream_get_contents($pipes[1]); - fclose($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[2]); - $return_value = proc_close($process); - - if ($stderr !== '') { + if (exec("pdftotext -raw " . escapeshellarg($filename) . " -", $text) === false) { header('Status: 500'); - header('Content-Length: ' . strlen($stderr)); - header('Content-Type: text/plain'); - exit($stderr); + header('Content-Length: 0'); + exit; } + $text = implode("\n", $text); + exec("pdfsig " . escapeshellarg($filename), $sig); + $sig = implode("\n", $sig); $r = preg_match('@([a-z]{2}) (https://webgate\.ec\.europa\.eu/tracesnt/directory/publication/organic-operator/(.*?)\.pdf) (\d+) / (\d+)@', $text, $matches); if ($r === 1) { @@ -189,6 +163,31 @@ if ($format === 'text') { $valid1 = implode('-', array_reverse(explode('/', $matches[0][0]))); $valid2 = implode('-', array_reverse(explode('/', $matches[1][0]))); + $sigs = []; + foreach (array_slice(explode("\nSignature #", $sig), 1) as $s) { + $sData = []; + $sData2 = []; + preg_match_all('/\n {2}- (([^:\n]*): )?([^\n]*)/', $s, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + if (strlen($m[2]) === 0) { + $sData2[] = $m[3]; + } else { + $sData[$m[2]] = $m[3]; + } + } + $sigs[] = [ + 'signerCommonName' => $sData['Signer Certificate Common Name'], + 'valid' => $sData['Signature Validation'] === 'Signature is Valid.', + 'trusted' => $sData['Certificate Validation'] === 'Certificate is Trusted.', + 'totalDocument' => in_array('Total document signed', $sData2), + 'timestamp' => gmdate('Y-m-d\TH:i:s\Z', strtotime($sData['Signing Time'])), + 'type' => $sData['Signature Type'], + 'hashAlgorithm' => $sData['Signing Hash Algorithm'], + 'signerDistinguishedName' => $sData['Signer full Distinguished Name'], + 'fieldName' => $sData['Signature Field Name'], + ]; + } + echo "{\"type\":\"traces\",\"lang\":\"$lang\",\"id\":\"$certId\",\"status\":\"$statusMap[$status]\""; echo ",\n \"operator\":{\"id\":" . jenc($operatorId). ',"groupOfOperators":' . jenc(!str_starts_with($data['I.2'], '☑')) . @@ -207,23 +206,34 @@ if ($format === 'text') { ",\n \"productCategories\":" . jenc($products) . ",\n \"validFrom\":" . jenc($valid1) . ',"validTo":' . jenc($valid2) . - ",\n \"url\":\"$certUrl\"\n}\n"; - } else { - echo "{\"type\":\"unknown\"}\n"; + ",\n \"url\":\"$certUrl\"" . + ",\n \"digitalSignatures\":" . jenc($sigs) . + "\n}\n"; + exit; } -} else { - $fd_spec = [ - 0 => ["pipe", "r"], // stdin - 1 => ["pipe", "w"], // stdout - 2 => ["pipe", "w"], // stderr - 3 => ["pipe", "w"], // headers - ]; - $process = proc_open(['curl', '-s', '-D', '/dev/fd/3', $url], $fd_spec, $pipes); - fclose($pipes[0]); + if (preg_match('/AT-BIO-[0-9]{3}/', $text, $matches) === 1) { + $authorityId = $matches[0]; + $certId = null; + $certNr = null; + if (preg_match("/$authorityId\.040-[0-9]{7}\.[0-9]{4}\.[0-9]{3}/", $text, $matches) === 1) + $certId = $matches[0]; + if (preg_match_all("/\b[0-9]+([._-])[0-9]+\g{-1}[0-9]+\b/", $text, $matches, PREG_SET_ORDER) !== false) { + foreach ($matches as $m) { + if (strlen($m[0]) > 10 && !str_ends_with($certId, $m[0])) + $certNr = $m[0]; + } + } + echo "{\"type\":\"$authorityId\",\"lang\":\"de\",\"id\":" . jenc($certId) . ",\"nr\":" . jenc($certNr); + echo ",\n \"operator\":{},\n \"authority\":{\"id\":" . jenc($authorityId) . "}}\n"; + exit; + } + + echo "{\"type\":\"unknown\"}\n"; +} else { $headers = []; $status_code = null; - while (($line = fgets($pipes[3])) !== false) { + foreach (explode("\n", file_get_contents($headerfilename)) as $line) { if (trim($line) === '') break; if ($status_code === null) { $status_code = intval(explode(' ', $line)[1]); @@ -236,9 +246,8 @@ if ($format === 'text') { $v = trim($v); $headers[$k] = $v; } - fclose($pipes[3]); - if ($status_code === 200 && str_starts_with($headers['content-type'], "application/pdf")) { + if (str_starts_with($headers['content-type'], "application/pdf")) { header('Content-Type: application/pdf'); $content_length = null; if (isset($headers['content-length'])) { @@ -246,20 +255,15 @@ if ($format === 'text') { header('Content-Length: ' . $headers['content-length']); } $parts = explode('/', $url); - $filename = $parts[sizeof($parts) - 1]; + $realFilename = $parts[sizeof($parts) - 1]; if (isset($headers['content-disposition'])) { preg_match('@filename="(.*?)"@', $headers['content-disposition'], $matches); - $filename = $matches[1]; + $realFilename = $matches[1]; } - header('Content-Disposition: inline; filename="' . $filename . '"'); - fpassthru($pipes[1]); + header('Content-Disposition: inline; filename="' . $realFilename . '"'); + fpassthru($file); } else { header('Status: 500'); header('Content-Length: 0'); } - fclose($pipes[1]); - - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[2]); - $return_value = proc_close($process); }