Ture La Manna 6 mesi fa
commit
cbe5c55f6d
  1. 2
      .gitignore
  2. 8
      App/Config/database.php.example
  3. 5
      App/Config/site.php.example
  4. 141
      App/Controllers/ControllerV1.php
  5. 29
      App/Core/AutoLoader.php
  6. 30
      App/Core/Config.php
  7. 109
      App/Core/Controller.php
  8. 160
      App/Core/Model.php
  9. 96
      App/Models/Cns.php
  10. 18
      App/Models/SiteConfig.php
  11. 31
      App/Models/Storage.php
  12. 137
      App/Models/User.php
  13. 6
      App/bootstrap.php
  14. 8
      App/cronjob
  15. 5
      doint.sh
  16. 6
      html/api/v1/.htaccess
  17. 8
      html/api/v1/index.php
  18. 21
      html/cns/index.php
  19. 13
      html/cns/policy-error.html
  20. 8975
      html/css/bootstrap.css
  21. 39
      html/css/custom.css
  22. 38
      html/errors/generic.php
  23. 22
      html/errors/init.php
  24. 39
      html/errors/no-cns.php
  25. BIN
      html/images/cns-icon.png
  26. BIN
      html/images/logo-regione.png
  27. BIN
      html/images/spid-icon.png
  28. 63
      html/index.php
  29. 4
      html/js/jquery.min.js
  30. 12
      html/openidc/jwks.json
  31. 27
      html/openidc/metadata.json
  32. 2
      html/robots.txt
  33. 23
      html/spid/index.php
  34. 18
      int.php
  35. 1
      setup/cron/api-cleanup
  36. 1
      setup/cron/auto-update-gov-certificates
  37. 20
      setup/file/0certbot.conf
  38. 70
      setup/file/1autenticazione.conf
  39. 63
      setup/file/autenticazione.sql
  40. 22
      setup/file/ssl-params.conf
  41. 67
      setup/metadata/crea_metadata.php
  42. 52
      setup/metadata/private.pem
  43. BIN
      setup/metadata/privatekeys.pfx
  44. 14
      setup/metadata/public.pem
  45. 77
      setup/setup.sh
  46. 39
      setup/usr-local-bin/auto-update-gov-certificates
  47. 146
      setup/usr-local-bin/parse-gov-certs.py
  48. 12
      setup/usr-local-bin/project_env.sh

2
.gitignore vendored

@ -0,0 +1,2 @@
App/Config/**.php
html/cns/policy/**

8
App/Config/database.php.example

@ -0,0 +1,8 @@
<?php
return [
'host' => 'localhost',
'database' => 'database',
'username' => 'username',
'password' => 'password',
];

5
App/Config/site.php.example

@ -0,0 +1,5 @@
<?php
return [
'key' => 'CHANGE_ME',
];

141
App/Controllers/ControllerV1.php

@ -0,0 +1,141 @@
<?php
namespace MwgAuth\Controllers;
use MwgAuth\Core\Controller;
use MwgAuth\Models\User;
class ControllerV1 extends Controller
{
public static function login(): string
{
self::requireMethod('POST');
$input = self::getPost();
if (null === $input) {
return self::error(self::ERROR_INVALID_INPUT);
}
$userName = $input[self::PARAMETER_USERNAME] ?? null;
if (null === $userName) {
return self::error(self::ERROR_MISSING_PARAMETER, self::PARAMETER_USERNAME);
}
$password = $input[self::PARAMETER_PASSWORD] ?? null;
if (null === $password) {
return self::error(self::ERROR_MISSING_PARAMETER, self::PARAMETER_PASSWORD);
}
$user = User::load($userName, $password, $_SERVER['REMOTE_ADDR']);
if (null === $user) {
return self::error(self::ERROR_INVALID_PASSWORD);
}
return self::response([self::PARAMETER_AUTH => $user->getAuth()]);
}
public static function getToken(): string
{
self::requireMethod('POST');
$input = self::getPost();
if (null === $input) {
return self::error(self::ERROR_INVALID_INPUT);
}
$auth = $input[self::PARAMETER_AUTH] ?? null;
if (null === $auth) {
return self::error(self::ERROR_MISSING_PARAMETER, self::PARAMETER_AUTH);
}
$callback = $input[self::PARAMETER_CALLBACK_URL] ?? null;
if (null === $callback) {
return self::error(self::ERROR_MISSING_PARAMETER, self::PARAMETER_CALLBACK_URL);
}
$user = User::verifyAuth($auth);
if (null === $user) {
return self::error(self::ERROR_INVALID_AUTH);
}
$token = $user->getToken($callback);
return self::response([self::PARAMETER_TOKEN => $token]);
}
public static function getUser(): string
{
self::requireMethod('POST');
$input = self::getPost();
if (null === $input) {
return self::error(self::ERROR_INVALID_INPUT);
}
$auth = $input[self::PARAMETER_AUTH] ?? null;
if (null === $auth) {
return self::error(self::ERROR_MISSING_PARAMETER, self::PARAMETER_AUTH);
}
$user = User::verifyAuth($auth);
if (null === $user) {
return self::error(self::ERROR_INVALID_AUTH);
}
$token = $input[self::PARAMETER_TOKEN] ?? null;
if (null === $token) {
return self::error(self::ERROR_MISSING_PARAMETER, self::PARAMETER_TOKEN);
}
if (!$user->verifyToken($token)) {
return self::error(self::ERROR_INVALID_TOKEN);
}
$userData = $user->getLoginData($token);
if (null === $userData) {
return self::error(self::ERROR_INVALID_LOGIN);
}
return self::response($userData);
}
private static function json(array $values): string
{
return json_encode($values, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
private static function response(array $values): string
{
$response = [
self::LABEL_ERROR_CODE => self::ERROR_OK,
self::LABEL_STATUS => self::STATUS_OK,
self::LABEL_DESCRIPTION => self::ERROR_DESCRIPTIONS[self::ERROR_OK],
];
$response += $values;
return self::json($response);
}
private static function error(int $errorCode, ...$parameters): string
{
$status = $errorCode == self::ERROR_OK ? self::STATUS_OK : self::STATUS_ERROR;
return self::json([
self::LABEL_ERROR_CODE => $errorCode,
self::LABEL_STATUS => $status,
self::LABEL_DESCRIPTION => vsprintf(self::ERROR_DESCRIPTIONS[$errorCode], $parameters),
]);
}
protected const LABEL_ERROR_CODE = 'code';
protected const LABEL_DESCRIPTION = 'description';
protected const LABEL_STATUS = 'status';
protected const ERROR_OK = 0;
protected const ERROR_INVALID_AUTH = 1;
protected const ERROR_INVALID_LOGIN = 2;
protected const ERROR_INVALID_TOKEN = 3;
protected const ERROR_INVALID_INPUT = 4;
protected const ERROR_MISSING_PARAMETER = 5;
protected const ERROR_INVALID_PASSWORD = 6;
protected const STATUS_OK = 'success';
protected const STATUS_ERROR = 'error';
protected const ERROR_DESCRIPTIONS = [
self::ERROR_OK => 'success',
self::ERROR_INVALID_AUTH => 'invalid or expired authorization',
self::ERROR_INVALID_LOGIN => 'no login associated with this token',
self::ERROR_INVALID_TOKEN => 'invalid or expired token',
self::ERROR_INVALID_INPUT => 'invalid input',
self::ERROR_MISSING_PARAMETER => "parameter '%s' is missing",
self::ERROR_INVALID_PASSWORD => 'invalid username or password',
];
protected const PARAMETER_AUTH = 'auth';
protected const PARAMETER_CALLBACK_URL = 'callback';
protected const PARAMETER_REDIRECT = 'redirect';
protected const PARAMETER_TOKEN = 'token';
protected const PARAMETER_USERNAME = 'username';
protected const PARAMETER_PASSWORD = 'password';
}

29
App/Core/AutoLoader.php

@ -0,0 +1,29 @@
<?php
namespace MwgAuth\Core;
class AutoLoader
{
private static $patterns = [];
private static $replacements = [];
public static function register()
{
if (empty(self::$patterns)) {
self::$patterns = [
'/^' . explode('\\', self::class)[0] . '/',
'/\\\\/',
];
}
if (empty(self::$replacements)) {
self::$replacements = ['', '/'];
}
spl_autoload_register(self::class . '::autoLoader');
}
public static function autoLoader(string $class)
{
$class = preg_replace(self::$patterns, self::$replacements, $class);
require_once(dirname(__DIR__) . $class . '.php');
}
}

30
App/Core/Config.php

@ -0,0 +1,30 @@
<?php
namespace MwgAuth\Core;
class Config
{
private static $config = [];
private static function load(): void
{
if (empty(self::$config)) {
foreach (glob(dirname(__DIR__) . '/Config/*.php') as $config) {
$section = pathinfo($config, PATHINFO_FILENAME);
self::$config[$section] = include($config);
}
}
}
public static function getSection(string $section): array
{
self::load();
return self::$config[$section] ?? [];
}
public static function getValue(string $section, string $name): ?string
{
self::load();
return self::$config[$section][$name] ?? null;
}
}

109
App/Core/Controller.php

@ -0,0 +1,109 @@
<?php
namespace MwgAuth\Core;
class Controller
{
private static $charset = 'UTF-8';
private static $contentType = 'application/json';
private static $responseCode = 200;
public static function run(string $function): void
{
if ($function === '') {
$function = 'index';
} else {
$function = self::camelize($function);
}
if (method_exists(static::class, $function)) {
$html = static::$function();
$header = 'Content-Type: ' . self::$contentType;
if (null !== self::$charset) {
$header .= "; charset=" . self::$charset;
}
header($header, true, self::$responseCode);
echo $html;
} else {
self::page404();
}
}
protected static function camelize(string $input): string
{
$ret = [];
foreach (explode('_', $input) as $part) {
$ret[] = ucfirst($part);
}
return implode('', $ret);
}
protected static function getPost(): ?array
{
$input = file_get_contents('php://input');
return json_decode($input, true);
}
protected static function requireMethod(string $method): void
{
if ($method !== $_SERVER['REQUEST_METHOD']) {
header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed', true, 405);
die("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n" .
"<html><head>\n" .
"<title>405 Method Not Allowed</title>\n" .
"</head><body>\n" .
"<h1>Method Not Allowed</h1>\n" .
"<p>The requested method " . $_SERVER['REQUEST_METHOD'] . " is not allowed for this resource.</p>\n" .
self::serverSignature());
}
}
protected static function page404(): string
{
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found', true, 404);
die("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n" .
"<html><head>\n" .
"<title>404 Not Found</title>\n" .
"</head><body>\n" .
"<h1>Not Found</h1>\n" .
"<p>The requested URL was not found on this server.</p>\n" .
self::serverSignature());
}
public static function redirect(string $location)
{
header($_SERVER['SERVER_PROTOCOL'] . ' 302 Found', true, 302);
header('Location: ' . $location, true);
die("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n" .
"<html><head>\n" .
"<title>302 Found</title>\n" .
"</head><body>\n" .
"<h1>Found</h1>\n" .
"<p>The document has moved <a href=\"" . $location . "\">here</a>.</p>\n" .
self::serverSignature());
}
private static function serverSignature(): string
{
return
"<hr>\n" .
"<address>" . $_SERVER['SERVER_SOFTWARE'] . " Server at " . $_SERVER['SERVER_NAME'] . " Port " .
$_SERVER['SERVER_PORT'] . "</address>\n" .
"</body></html>\n";
}
protected static function setCharset(string $charset): void
{
self::$charset = $charset;
}
protected static function setContentType(string $contentType): void
{
self::$contentType = $contentType;
}
protected static function setResponseCode(string $responseCode): void
{
self::$responseCode = $responseCode;
}
}

160
App/Core/Model.php

@ -0,0 +1,160 @@
<?php
namespace MwgAuth\Core;
use PDO;
use PDOStatement;
class Model
{
private static $database = null;
private static $lastErrorMessage = '';
private static function connect(): void
{
if (null === self::$database) {
$config = Config::getSection('database');
self::$database = new PDO(
'mysql:host=' . $config['host'] . ';dbname=' . $config['database'],
$config['username'],
$config['password'],
);
}
}
protected static function delete(string $table, array $conditions): bool
{
$where = $parameters = [];
foreach ($conditions as $condition) {
switch (count($condition)) {
case 1:
$where[] = '`' . $condition[0] . '`';
break;
case 2:
$where[] = '`' . $condition[0] . '`=:' . $condition[0];
$parameters[':' . $condition[0]] = $condition[1];
break;
case 3:
$where[] = '`' . $condition[0] . '`' . $condition[1] . ':' . $condition[0];
$parameters[':' . $condition[0]] = $condition[2];
break;
default:
return false;
}
}
$sql = "DELETE FROM `$table` WHERE (" . implode(") AND (", $where) . ")";
$statement = self::prepare($sql, $parameters);
return self::execute($statement);
}
private static function execute(PDOStatement $statement): bool
{
if ($ret = $statement->execute()) {
self::$lastErrorMessage = '';
} else {
$errorInfo = $statement->errorInfo();
self::$lastErrorMessage = $errorInfo[0] . ': ' . $errorInfo[2];
}
return $ret;
}
protected static function getLastErrorMessage(): string
{
return self::$lastErrorMessage;
}
private static function getParameters(array $conditions): array
{
$parameters = [];
foreach ($conditions as $field => $value) {
$parameters[":$field"] = $value;
}
return $parameters;
}
private static function getSelectQuery(string $table, array $fields, array $conditions): string
{
$where = [];
foreach ($conditions as $field => $value) {
$where[] = "(`$field`=:$field)";
}
return "SELECT `" . implode('`,`', $fields) . "` FROM `$table` WHERE " . implode(' AND ', $where);
}
protected static function insert(string $table, array $fields): bool
{
$fields['created_at'] = $fields['modified_at'] = date('Y-m-d H:i:s');
$parameters = self::getParameters($fields);
$sql = "INSERT INTO `$table`(`" . implode('`,`', array_keys($fields)) . "`) VALUES (" .
implode(',', array_keys($parameters)) . ')';
$statement = self::prepare($sql, $parameters);
return self::execute($statement);
}
private static function prepare(string $sql, array $parameters): PDOStatement
{
self::connect();
$statement = self::$database->prepare($sql);
foreach ($parameters as $name => $value) {
if (is_bool($value)) {
$type = PDO::PARAM_BOOL;
} elseif (is_int($value)) {
$type = PDO::PARAM_INT;
} else {
$type = PDO::PARAM_STR;
}
$statement->bindValue($name, $value, $type);
}
return $statement;
}
protected static function select(string $table, array $fields, array $conditions): ?array
{
$parameters = self::getParameters($conditions);
$sql = self::getSelectQuery($table, $fields, $conditions);
$statement = self::prepare($sql, $parameters);
self::execute($statement);
$ret = $statement->fetchAll(PDO::FETCH_ASSOC);
$statement->closeCursor();
return false === $ret ? null : $ret;
}
protected static function selectOne(string $table, array $fields, array $conditions): ?array
{
$parameters = self::getParameters($conditions);
$sql = self::getSelectQuery($table, $fields, $conditions) . ' LIMIT 1';
$statement = self::prepare($sql, $parameters);
self::execute($statement);
$ret = $statement->fetch(PDO::FETCH_ASSOC);
$statement->closeCursor();
return false === $ret ? null : $ret;
}
protected static function selectSingleValue(string $table, string $field, array $conditions)
{
$parameters = self::getParameters($conditions);
$sql = self::getSelectQuery($table, [$field], $conditions) . ' LIMIT 1';
$statement = self::prepare($sql, $parameters);
self::execute($statement);
$ret = $statement->fetchColumn();
$statement->closeCursor();
return false === $ret ? null : $ret;
}
protected static function update(string $table, array $fields, array $conditions): bool
{
$fields['modified_at'] = date('Y-m-d H:i:s');
$where = $parameters = $set = [];
foreach ($fields as $field => $value) {
$set[] = "`$field`=:p$field";
$parameters[":p$field"] = $value;
}
foreach ($conditions as $field => $value) {
$where[] = "(`$field`=:w$field)";
$parameters[":w$field"] = $value;
}
$sql = "UPDATE `$table` SET " . implode(',', $set) . " WHERE " . implode(',', $where);
$statement = self::prepare($sql, $parameters);
return self::execute($statement);
}
}

96
App/Models/Cns.php

@ -0,0 +1,96 @@
<?php
namespace MwgAuth\Models;
use MwgAuth\Core\Model;
class Cns extends Model
{
private static $policyValide = [
'1.3.76.16.2.1', // CNS
'1.3.76.47.4', // CIE
'1.3.6.1.4.1.29741.1.1.10', // CNS-Like
'1.3.159.1.10.1', // Actalis
'1.3.76.36.1.1.3', // CNS-Like Infocert
];
private $certificato;
private $codiceFiscale;
public function __construct(string $certificato)
{
if (null !== $certificato) {
$this->certificato = openssl_x509_parse($certificato, false);
$this->verificaCertificato();
// https://www.agid.gov.it/sites/default/files/repository_files/documentazione_trasparenza/strutturacertificatoautenticazionecns_v1.1_.pdf
if (isset($this->certificato['subject']['serialNumber'])) {
$this->codiceFiscale = preg_replace('/^[A-Za-z]{2}:/', '', $this->certificato['subject']['serialNumber']);
} else {
$this->codiceFiscale = explode('/', $this->certificato['subject']['commonName'])[0];
}
}
}
public function getCertificato(): array
{
return $this->certificato;
}
public function getCodiceFiscale(): ?string
{
return $this->codiceFiscale;
}
public function getCognome(): ?string
{
if (isset($this->certificato['subject']['surname'])) {
$ret = $this->certificato['subject']['surname'];
} elseif (isset($this->certificato['subject']['organizationalUnitName'])) {
$ret = $this->certificato['subject']['organizationalUnitName'];
if (is_array($ret)) {
$ret = implode('/', $ret);
}
} else {
$ret = $this->codiceFiscale;
}
return $ret;
}
public function getNome(): ?string
{
return $this->certificato['subject']['givenName'] ?? $this->codiceFiscale;
}
public function verificaPolicyCertificato(): bool
{
preg_match_all(
'/Policy: ([0-9\.]+)/',
$this->certificato['extensions']['certificatePolicies'],
$policies
);
foreach ($policies[1] as $policy) {
if (in_array($policy, self::$policyValide)) {
return true;
}
}
$this->salvaCertificato();
return false;
}
private function verificaCertificato()
{
// if (!isset($this->certificato['subject']['commonName']) ||
// !isset($this->certificato['subject']['surname']) ||
// !isset($this->certificato['subject']['givenName']) ||
// isset($this->certificato['subject']['serialNumber']) ||
// !isset($this->certificato['extensions']['certificatePolicies'])) {
$this->salvaCertificato();
// }
}
private function salvaCertificato()
{
$file = '/var/www/html/cns/policy/cert-' . sha1($this->certificato['subject']['commonName']);
@file_put_contents($file . '.txt', print_r($this->certificato, true));
@file_put_contents($file . '.cer', $_SERVER['SSL_CLIENT_CERT']);
}
}

18
App/Models/SiteConfig.php

@ -0,0 +1,18 @@
<?php
namespace MwgAuth\Models;
use MwgAuth\Core\Model;
class SiteConfig extends Model
{
private static $config = [];
public static function getSetting(string $key): string
{
if (!isset(self::$config[$key])) {
self::$config[$key] = self::selectSingleValue('config', 'value', ['key' >= $key]);
}
return self::$config[$key];
}
}

31
App/Models/Storage.php

@ -0,0 +1,31 @@
<?php
namespace MwgAuth\Models;
use MwgAuth\Core\Model;
class Storage extends Model
{
public static function fileGetContents(string $fileName): ?string
{
$fullName = self::getFullName($fileName);
if (file_exists($fullName)) {
return file_get_contents($fullName);
}
return null;
}
public static function filePutContents(string $fileName, string $data, int $flags = 0)
{
$fullName = self::getFullName($fileName);
$umask = umask(0);
mkdir(dirname($fullName), 0777, true);
file_put_contents($fullName, $data, $flags);
umask($umask);
}
private static function getFullName(string $fileName): string
{
return $_SERVER['DOCUMENT_ROOT'] . '/storage/' . $fileName;
}
}

137
App/Models/User.php

@ -0,0 +1,137 @@
<?php
namespace MwgAuth\Models;
use MwgAuth\Core\Config;
use MwgAuth\Core\Model;
class User extends Model
{
private $id;
private $auth;
private $userName;
public static function load(string $userName, string $password, string $remoteAddress): ?User
{
$password = self::hashPassword($password);
#file_put_contents('/var/www/storage/password.txt', "$userName\n$password\n$remoteAddress\n");
$rows = self::select('users', ['id', 'username', 'remote'], ['username' => $userName, 'password' => $password]);
foreach ($rows as $row) {
if (fnmatch($row['remote'], $remoteAddress)) {
return new User($row['id'], $row['username']);
}
}
return null;
}
public function __construct(int $id, string $userName, ?string $auth = null)
{
$this->id = $id;
$this->userName = $userName;
if (null === $auth) {
do {
$this->auth = sha1(random_int(PHP_INT_MIN, PHP_INT_MAX)); // /dev/urandom
$result = $this->insert('sessions', [
'auth' => $this->auth,
'user_id' => $this->id,
'username' => $this->userName,
]);
} while (!$result);
} else {
$this->auth = $auth;
}
}
public static function cleanUp(): void
{
$timestamp = date('Y-m-d H:i:s', time() - 900);
self::delete('tokens', [['modified_at', '<', $timestamp]]);
self::delete('sessions', [['modified_at', '<', $timestamp]]);
}
public function getAuth(): string
{
return $this->auth;
}
public static function getCallback(string $token): ?string
{
return self::selectSingleValue('tokens', 'callback', ['token' => $token]);
}
public function getLoginData(string $token): ?array
{
$ret = $this->select('logins', ['codice_fiscale', 'nome', 'cognome'], ['token' => $token]);
return empty($ret) ? null : $ret[0];
}
public function getId(): int
{
return $this->id;
}
public function getToken(string $callback): string
{
do {
$token = sha1(random_int(PHP_INT_MIN, PHP_INT_MAX)); // /dev/urandom
$result = $this->insert('tokens', [
'token' => $token,
'auth' => $this->auth,
'callback' => $callback,
]);
} while (!$result);
$this->touchAuth($this->auth);
return $token;
}
public function getUserName(): string
{
return $this->userName;
}
public static function setLoginData(string $token, string $codiceFiscale, string $nome, string $cognome): bool
{
return self::insert('logins', [
'token' => $token,
'codice_fiscale' => $codiceFiscale,
'nome' => $nome,
'cognome' => $cognome,
]);
}
public static function verifyAuth(string $auth): ?User
{
$row = self::selectOne('sessions', ['user_id', 'username', 'auth'], ['auth' => $auth]);
if (null === $row) {
return null;
}
self::touchAuth($row['auth']);
return new User($row['user_id'], $row['username'], $row['auth']);
}
public function verifyToken(string $token): bool
{
$row = $this->select('tokens', ['token'], ['auth' => $this->auth, 'token' => $token]);
return count($row) > 0;
}
private static function getSiteKey(): string
{
return Config::getValue('site', 'key');
}
private static function hashPassword(string $password): string
{
$siteKey = self::getSiteKey();
return hash('sha256', $siteKey . $password);
}
/**
* Ha il solo effetto di aggiornare il campo "modified_at" della sessione
*/
private static function touchAuth(string $auth): bool
{
return self::update('sessions', [], ['auth' => $auth]);
}
}

6
App/bootstrap.php

@ -0,0 +1,6 @@
<?php
namespace MwgAuth\Core;
require_once(__DIR__.'/Core/AutoLoader.php');
AutoLoader::register();

8
App/cronjob

@ -0,0 +1,8 @@
#!/usr/bin/env php
<?php
use MwgAuth\Models\User;
require('bootstrap.php');
User::cleanUp();

5
doint.sh

@ -0,0 +1,5 @@
#!/bin/bash
for a in $(seq 0 $(($(ls html/cns/policy/*.txt | wc -l) - 1))); do
echo $a
php int.php $a | grep 'Codice fiscale:'
done

6
html/api/v1/.htaccess

@ -0,0 +1,6 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]
</IfModule>

8
html/api/v1/index.php

@ -0,0 +1,8 @@
<?php
use MwgAuth\Controllers\ControllerV1;
require(dirname($_SERVER['DOCUMENT_ROOT']) . '/App/bootstrap.php');
preg_match('@/([_0-9A-Za-z]+)\.cgi$@', $_SERVER['REQUEST_URI'], $matches);
$function = $matches[1] ?? '';
ControllerV1::run($function);

21
html/cns/index.php

@ -0,0 +1,21 @@
<?php
use MwgAuth\Core\Controller;
use MwgAuth\Models\Cns;
use MwgAuth\Models\User;
require_once(dirname($_SERVER['DOCUMENT_ROOT']) . '/App/bootstrap.php');
$cns = new Cns($_SERVER['SSL_CLIENT_CERT']);
if (!$cns->verificaPolicyCertificato()) {
Controller::redirect('/cns/policy-error.html');
}
$token = $_GET['t'] ?? null;
if (null === $token) {
include($_SERVER['DOCUMENT_ROOT'] . '/errors/generic.php');
} else {
if (null !== $cns->getCodiceFiscale()) {
User::setLoginData($token, $cns->getCodiceFiscale(), $cns->getNome(), $cns->getCognome());
}
$callback = User::getCallback($token);
Controller::redirect($callback);
}

13
html/cns/policy-error.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Policy Error</title>
</head>
<body>
<h1>Errore Certificate Policies (Object ID: 2.5.29.32)<br></h1>
<h2>Non è stato possibile autenticarti.</h2>
<p>Il certificato presentato è valido ma non rispetta le <strong>Certificate Policies (Object ID:
2.5.29.32)</strong> previste per la CIE e TS-CNS.</p>
</body>
</html>

8975
html/css/bootstrap.css vendored

File diff soppresso perché troppo grande Load Diff

39
html/css/custom.css

@ -0,0 +1,39 @@
* {
font-family: 'Titillium Web', sans-serif;
}
.btn-label, #spid-button .spid-button-icon {
position: relative;
left: -12px;
display: inline-block;
padding: 6px 12px;
background: rgba(0,0,0,0.15);
border-radius: 3px 0 0 3px;
}
.btn-labeled {
padding-top: 0;
padding-bottom: 0;
}
.btn {
margin-bottom:10px;
border-radius: 0;
font-size: 16px;
}
hr.solid {
border-top: 1px solid #ccc;
width: 40%;
margin-left: 0;
}
#spid-button .spid-button {
margin: 0;
border-radius: 0;
padding: 0 0.75rem;
height: auto;
font-size: 16px;
}
.spid-button-text{
vertical-align: initial !important;
font-weight: normal;
}

38
html/errors/generic.php

@ -0,0 +1,38 @@
<?php require_once('init.php'); ?><!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Autenticazione Regione Siciliana</title>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap">
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/custom.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
<div class="row mt-3">
<div class="col-xs-12 col-sm-12 pb-4">
<img src="/images/logo-regione.png">
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12">
<h2>Errore di autenticazione</h2>
<p>Si è verificato un errore inatteso durante l'autenticazione. Si prega di riprovare più tardi.</p>
<?php if (null !== $callback) { ?>
<div class="text-center d-block">
<a class="btn btn-primary btn-lg mt-4 text-center " href="<?= $callback ?>">Torna al portale</a>
</div>
<?php } ?>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-sm-12 pb-4 text-center">
<p>Regione Siciliana - D.R.T.</p>
</div>
</div>
</div>
</body>
</html>

22
html/errors/init.php

@ -0,0 +1,22 @@
<?php
use MwgAuth\Models\User;
require_once(dirname($_SERVER['DOCUMENT_ROOT']) . '/App/bootstrap.php');
$callback = null;
parse_str($_SERVER['REDIRECT_QUERY_STRING'], $get);
$token = $get['t'] ?? null;
if (null !== $token) {
$callback = User::getCallback($token);
$parsed = parse_url($callback);
if (isset($parsed['scheme']) && isset($parsed['host'])) {
$callback = $parsed['scheme'] . '://' . $parsed['host'];
if (($parsed['scheme'] === 'http' && $parsed['port'] != 80)
|| ($parsed['scheme'] === 'https' && $parsed['port'] != 443)
) {
$callback .= ':' . $parsed['port'];
}
$callback .= '/';
}
}

39
html/errors/no-cns.php

@ -0,0 +1,39 @@
<?php require_once('init.php'); ?><!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Autenticazione Regione Siciliana</title>
<link href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap" rel="stylesheet">
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/custom.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
<div class="row mt-3">
<div class="col-xs-12 col-sm-12 pb-4">
<img src="/images/logo-regione.png">
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12">
<h2>Errore autenticazione con CNS</h2>
<p>Non è stato possibile autenticarti. Verifica che il lettore sia correttamente collegato e
funzionante, e che la carta CIE o TS-CNS sia correttamente inserita o appoggiata sul lettore nel
momento in cui procedi con l'accesso.</p>
<?php if (null !== $callback) { ?>
<div class="text-center d-block">
<a class="btn btn-primary btn-lg mt-4 text-center " href="<?= $callback ?>">Torna al portale</a>
</div>
<?php } ?>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-sm-12 pb-4 text-center">
<p>Regione Siciliana - D.R.T.</p>
</div>
</div>
</div>
</body>
</html>

BIN
html/images/cns-icon.png

File binario non mostrato.

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 24 KiB

BIN
html/images/logo-regione.png

File binario non mostrato.

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 20 KiB

BIN
html/images/spid-icon.png

File binario non mostrato.

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 2.4 KiB

63
html/index.php

@ -0,0 +1,63 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Autenticazione Regione Siciliana</title>
<link href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap" rel="stylesheet">
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/custom.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
<div class="row mt-3">
<div class="col-12 pb-4">
<img src="/images/logo-regione.png">
</div>
</div>
<div class="row">
<div class="col-md-6 col-xs-12">
<h2>Accedi con CNS</h2>
<hr class="solid">
<p>
La Carta Nazionale dei Servizi o CNS è una smart card o una chiavetta USB che contiene un
"certificato digitale" di autenticazione personale, utile per accedere ai servizi online della
Regione Siciliana.
</p>
<form action="/cns/" method="GET">
<input type="hidden" name="t" value="<?= @$_GET['t'] ?>">
<button type="submit" class="btn btn-labeled btn-primary">
<span class="btn-label">
<img width="30" src="images/cns-icon.png">
</span> Entra con CNS
</button>
</form>
</div>
<div class="col-md-6 col-xs-12">
<h2>Accedi con SPID o CieID</h2>
<hr class="solid">
<p>
SPID è il sistema di accesso che consente di utilizzare, con un'identità digitale unica, i servizi
online della Pubblica Amministrazione e dei privati accreditati. Se sei già in possesso di
un'identità digitale, accedi con le credenziali del tuo gestore. Se non hai ancora un'identità
digitale, richiedila ad uno dei gestori.
</p>
<form action="/spid/" method="GET">
<input type="hidden" name="t" value="<?= @$_GET['t'] ?>">
<button type="submit" class="btn btn-labeled btn-primary">
<span class="btn-label">
<img width="30" src="images/spid-icon.png">
</span> Entra con SPID o CieID
</button>
</form>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-sm-12 pb-4 text-center">
<p>Regione Siciliana - D.R.T.</p>
</div>
</div>
</div>
<script src="/js/jquery.min.js"></script>
</body>
</html>

4
html/js/jquery.min.js vendored

File diff suppressed because one or more lines are too long

12
html/openidc/jwks.json

@ -0,0 +1,12 @@
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "auth",
"n": "0guv1uiJLOulgTJN7O4m+Hrf9N80ByPnncssc7ule+3XtSdgSbUEG4ztZq+TsBoAmAwsE44LxY4FvYxy39n1cZrw9EeuGY62bMulh9qdScHyHoUQwPGz6xLDQGJ0orr5THcFfLISGJdXutND9bNsxlPwC328Nbvym2SEp6NL8g77tGlg1dDGNybs6lAeUTUWS1qXbWEMl3VhJZa711BUstCujZsL2WyEsupVr80tWYobeCxuys/XBnbWbuPLiFMXCaGUnEzaoc1N4bF9AO6LrGphMG9JLAcPbNyF/DIFYn2Mlvy7eaXkyFIom7GJejpZexABhtKDMSLBHnDHaneLrkwmy0WogJpVSuTYL0nJdfPT1bT8ONx8YGikWQS/b8gtk/u/QQRBGtsBTA5a4Begyo8LmqLzeXBilqopviyIHpJ3kOU2iZo4Fj5qSjnAMT2IGuKKDhrjCGjito40GtYQEqkxbZf+Zz8QSuf4EWQt4oXLVYqD1WiSUk1MQ93ZT2MwCyh+jDG7m67dkcikDJa7yQYi7knDEGKmhBCBu3u/q2042vlSUe/OJIeBpqB16pZUZxPAKr1LUjURCKeHA2R7f3N3oC4jjVYrdgCvrOV/ZcJpsXR4EpyChgRtK0owXFCs+981xbnlBLSBazJwUxB+LbRJoHRiUc+TuqFM38CQJ9M=",
"e": "AQAB"
}
]
}

27
html/openidc/metadata.json

@ -0,0 +1,27 @@
{
"client_id": "https://auth.mwg.it",
"client_name": "Autenticazione Lavori Pubblici Sicilia",
"grant_types": [
"authorization_code",
"refresh_token"
],
"jwks_uri": "https://auth.mwg.it/openidc/jwks.json",
"jwks": {
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "auth",
"n": "0guv1uiJLOulgTJN7O4m+Hrf9N80ByPnncssc7ule+3XtSdgSbUEG4ztZq+TsBoAmAwsE44LxY4FvYxy39n1cZrw9EeuGY62bMulh9qdScHyHoUQwPGz6xLDQGJ0orr5THcFfLISGJdXutND9bNsxlPwC328Nbvym2SEp6NL8g77tGlg1dDGNybs6lAeUTUWS1qXbWEMl3VhJZa711BUstCujZsL2WyEsupVr80tWYobeCxuys/XBnbWbuPLiFMXCaGUnEzaoc1N4bF9AO6LrGphMG9JLAcPbNyF/DIFYn2Mlvy7eaXkyFIom7GJejpZexABhtKDMSLBHnDHaneLrkwmy0WogJpVSuTYL0nJdfPT1bT8ONx8YGikWQS/b8gtk/u/QQRBGtsBTA5a4Begyo8LmqLzeXBilqopviyIHpJ3kOU2iZo4Fj5qSjnAMT2IGuKKDhrjCGjito40GtYQEqkxbZf+Zz8QSuf4EWQt4oXLVYqD1WiSUk1MQ93ZT2MwCyh+jDG7m67dkcikDJa7yQYi7knDEGKmhBCBu3u/q2042vlSUe/OJIeBpqB16pZUZxPAKr1LUjURCKeHA2R7f3N3oC4jjVYrdgCvrOV/ZcJpsXR4EpyChgRtK0owXFCs+981xbnlBLSBazJwUxB+LbRJoHRiUc+TuqFM38CQJ9M=",
"e": "AQAB"
}
]
},
"redirect_uris": [
"https://auth.mwg.it/spid/callback/"
],
"response_types": [
"code"
]
}

2
html/robots.txt

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

23
html/spid/index.php

@ -0,0 +1,23 @@
<?php
use MwgAuth\Core\Controller;
use MwgAuth\Models\User;
require_once(dirname($_SERVER['DOCUMENT_ROOT']) . '/App/bootstrap.php');
$token = $_GET['t'] ?? null;
if ('' === ($token ?? '')) {
include($_SERVER['DOCUMENT_ROOT'] . '/errors/generic.php');
} else {
User::setLoginData(
$token,
$_SERVER['SPID_claim_fiscalNumber'],
$_SERVER['SPID_claim_given_name'],
$_SERVER['SPID_claim_family_name']
);
$callback = User::getCallback($token);
if (null === $callback) {
include($_SERVER['DOCUMENT_ROOT'] . '/errors/generic.php');
} else {
Controller::redirect($callback);
}
}

18
int.php

@ -0,0 +1,18 @@
<?php
if (php_sapi_name() != 'cli') {
die();
}
require_once('App/Core/Model.php');
require_once('App/Models/Cns.php');
$files = glob('html/cns/policy/*.cer');
if (count($files)) {
$idx = $argc > 1 ? intval($argv[1]) : 0;
$crt = file_get_contents($files[$idx]);
$cns = new \MwgAuth\Models\Cns($crt);
echo 'Codice fiscale: ' . $cns->getCodiceFiscale() . "\n";
echo 'Cognome: ' . $cns->getCognome() . "\n";
echo 'Nome: ' . $cns->getNome() . "\n";
echo 'Valido: ' . ($cns->verificaPolicyCertificato() ? 'true' : 'false') . "\n";
}

1
setup/cron/api-cleanup

@ -0,0 +1 @@
*/5 * * * * mwg /var/www/App/cronjob

1
setup/cron/auto-update-gov-certificates

@ -0,0 +1 @@
25 3 * * * root /usr/local/bin/auto-update-gov-certificates

20
setup/file/0certbot.conf

@ -0,0 +1,20 @@
<VirtualHost *:80>
ServerAdmin ${APACHE_SERVER_ADMIN}
ServerName ${SERVER_NAME}
DocumentRoot /var/www/certbot
RewriteEngine On
RewriteRule ^/$ https://%{HTTP_HOST}/ [L,R=301]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
<Directory "/var/www/certbot">
Require all granted
AllowOverride All
Options -Indexes
</Directory>
</VirtualHost>

70
setup/file/1autenticazione.conf

@ -0,0 +1,70 @@
SSLStrictSNIVHostCheck off
<VirtualHost *:${APACHE_SSL_PORT}>
ServerAdmin ${APACHE_SERVER_ADMIN}
ServerName ${SERVER_NAME}
DocumentRoot /var/www/html
LogLevel ${APACHE_LOG_LEVEL} ssl:${APACHE_SSL_LOG_LEVEL}
ErrorLog ${APACHE_LOG_DIR}/${SERVER_NAME}_error.log
CustomLog ${APACHE_LOG_DIR}/${SERVER_NAME}_access.log "%h %{SSL_PROTOCOL}x %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\""
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${SERVER_NAME}/${APACHE_SSL_CERTS}
SSLCertificateKeyFile /etc/letsencrypt/live/${SERVER_NAME}/${APACHE_SSL_PRIVATE}
SSLCACertificatePath /etc/ssl/itaca/
SSLInsecureRenegotiation on
OIDCProviderMetadataURL https://is-test.regione.sicilia.it:443/oauth2/token/.well-known/openid-configuration
OIDCClientID sJs9yMsWWz8K2Vkvys1F37zmtwwa
OIDCRedirectURI https://${SERVER_NAME}/spid/callback/
OIDCClientName SISMICA
OIDCClientContact admin@mwg.it
OIDCCryptoPassphrase zyNVgGtYvDh5d7RqsSvnz8EYR2a8
OIDCClaimPrefix SPID_claim_
OIDCPassClaimsAs environment
OIDCPublicKeyFiles "auth#/etc/apache2/keys/public.pem"
OIDCPrivateKeyFiles "auth#/etc/apache2/keys/private.pem"
<Directory "/var/www/html">
Require all granted
AllowOverride All
Options -MultiViews +SymLinksIfOwnerMatch
SSLOptions +StdEnvVars
#Order Allow,Deny
#Allow from 79.8.172.13
#Allow from 80.17.206.46
</Directory>
<Directory "/var/www/html/cns">
AllowOverride None
SSLVerifyClient ${APACHE_SSL_VERIFY_CLIENT}
SSLRenegBufferSize 16777216
SSLVerifyDepth 10
SSLOptions +ExportCertData +StdEnvVars +OptRenegotiate
SSLUserName SSL_CLIENT_S_DN_CN
<If "env('APACHE_SSL_VERIFY_CLIENT') =~ /optional|optional_no_ca/">
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !^SUCCESS$
RewriteRule .? - [F]
ErrorDocument 403 ${CLIENT_VERIFY_LANDING_PAGE}
</If>
</Directory>
<Directory "/var/www/html/spid">
Options -Indexes
</Directory>
<Location "/spid">
AuthType openid-connect
Require valid-user
</Location>
</VirtualHost>

63
setup/file/autenticazione.sql

@ -0,0 +1,63 @@
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`autenticazione` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `autenticazione`;
DROP TABLE IF EXISTS `logins`;
CREATE TABLE `logins` (
`token` char(40) NOT NULL,
`codice_fiscale` varchar(255) DEFAULT NULL,
`nome` varchar(255) DEFAULT NULL,
`cognome` varchar(255) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`modified_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `sessions`;
CREATE TABLE `sessions` (
`auth` char(40) NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`username` varchar(255) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`modified_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`auth`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tokens`;
CREATE TABLE `tokens` (
`token` char(40) NOT NULL,
`auth` char(40) NOT NULL,
`callback` TEXT DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`modified_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` char(64) NOT NULL,
`remote` varchar(45) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_username_remote_unique` (`username`,`remote`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `users`(`id`,`username`,`password`,`remote`,`created_at`,`updated_at`,`deleted_at`) VALUES
(1,'sismica','268633d46fdf00502dba60facce2ba5eec6a5d86c843c19f3f3b1ef8c0342ada','192.168.15.*',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,NULL),
(2,'sismica','268633d46fdf00502dba60facce2ba5eec6a5d86c843c19f3f3b1ef8c0342ada','79.8.172.13',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,NULL);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

22
setup/file/ssl-params.conf

@ -0,0 +1,22 @@
SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.3
SSLHonorCipherOrder On
# Disable preloading HSTS for now. You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Frame-Options DENY
Header always set X-Content-Type-Options nosniff
# Requires Apache >= 2.4
SSLCompression off
SSLUseStapling on
SSLStaplingCache "shmcb:logs/stapling-cache(150000)"
# Prevent browsers from failing if an OCSP server is temporarily broken.
SSLStaplingReturnResponderErrors off
SSLStaplingErrorCacheTimeout 60
SSLStaplingStandardCacheTimeout 36000
# Requires Apache >= 2.4.11
SSLSessionTickets Off

67
setup/metadata/crea_metadata.php

@ -0,0 +1,67 @@
<?php
//define('SERVER_NAME', 'autenticazione.lavoripubblici.sicilia.it');
define('SERVER_NAME', 'auth.mwg.it');
define('CLIENT_ID', 'https://' . SERVER_NAME);
define('CLIENT_NAME', 'Autenticazione Lavori Pubblici Sicilia');
define('REDIRECT_URIS', ['https://' . SERVER_NAME . '/spid/callback/']);
define('JWKS_URI', 'https://' . SERVER_NAME . '/openidc/jwks.json');
$distinguished_names = [
'countryName' => 'IT',
'stateOrProvinceName' => 'Sicilia',
"organizationName" => 'Regione Siciliana',
"organizationalUnitName" => 'Assessorato Infrastrutture, Trasporti e Mobilità',
"commonName" => SERVER_NAME,
"emailAddress" => 'info@lavoripubblici.sicilia.it',
];
define('OPENIDC_PATH', '/var/www/html/openidc');
define('PRIVATE_KEY', 'private.pem');
define('PUBLIC_KEY', 'public.pem');
if (file_exists(PRIVATE_KEY)) {
$private = openssl_pkey_get_private(file_get_contents(PRIVATE_KEY));
} else {
$private = openssl_pkey_new([
'private_key_bits' => 4096,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export_to_file($private, 'private.pem');
}
$details = openssl_pkey_get_details($private);
file_put_contents(PUBLIC_KEY, $details['key']);
// $csr = openssl_csr_new($distinguished_names, $private);
// $x509 = openssl_csr_sign($csr, null, $private, 3650, ['digest_alg' => 'sha512']);
// openssl_pkcs12_export_to_file($x509, 'privatekeys.pfx', $private, 'm4n1f');
$jwks = [
'keys' => [
[
'kty' => 'RSA',
'alg' => 'RS256',
'use' => 'sig',
'kid' => 'auth',
'n' => base64_encode($details['rsa']['n']),
'e' => base64_encode($details['rsa']['e']),
],
],
];
if (!is_dir(OPENIDC_PATH)) {
mkdir(OPENIDC_PATH, 0777, true);
}
file_put_contents(OPENIDC_PATH . '/jwks.json', json_encode($jwks, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
$metadata = json_encode([
'client_id' => CLIENT_ID,
'client_name' => CLIENT_NAME,
'grant_types' => [
'authorization_code',
'refresh_token',
],
'jwks_uri' => JWKS_URI,
'jwks' => $jwks,
'redirect_uris' => REDIRECT_URIS,
'response_types' => [
'code',
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(OPENIDC_PATH . '/metadata.json', $metadata);

52
setup/metadata/private.pem

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
VUCvqPCuPs2rHHJ8y/0686d72gU3+gg=
-----END PRIVATE KEY-----

BIN
setup/metadata/privatekeys.pfx

File binario non mostrato.

14
setup/metadata/public.pem

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0guv1uiJLOulgTJN7O4m
+Hrf9N80ByPnncssc7ule+3XtSdgSbUEG4ztZq+TsBoAmAwsE44LxY4FvYxy39n1
cZrw9EeuGY62bMulh9qdScHyHoUQwPGz6xLDQGJ0orr5THcFfLISGJdXutND9bNs
xlPwC328Nbvym2SEp6NL8g77tGlg1dDGNybs6lAeUTUWS1qXbWEMl3VhJZa711BU
stCujZsL2WyEsupVr80tWYobeCxuys/XBnbWbuPLiFMXCaGUnEzaoc1N4bF9AO6L
rGphMG9JLAcPbNyF/DIFYn2Mlvy7eaXkyFIom7GJejpZexABhtKDMSLBHnDHaneL
rkwmy0WogJpVSuTYL0nJdfPT1bT8ONx8YGikWQS/b8gtk/u/QQRBGtsBTA5a4Beg
yo8LmqLzeXBilqopviyIHpJ3kOU2iZo4Fj5qSjnAMT2IGuKKDhrjCGjito40GtYQ
EqkxbZf+Zz8QSuf4EWQt4oXLVYqD1WiSUk1MQ93ZT2MwCyh+jDG7m67dkcikDJa7
yQYi7knDEGKmhBCBu3u/q2042vlSUe/OJIeBpqB16pZUZxPAKr1LUjURCKeHA2R7
f3N3oC4jjVYrdgCvrOV/ZcJpsXR4EpyChgRtK0owXFCs+981xbnlBLSBazJwUxB+
LbRJoHRiUc+TuqFM38CQJ9MCAwEAAQ==
-----END PUBLIC KEY-----

77
setup/setup.sh

@ -0,0 +1,77 @@
#!/bin/sh -e
set -x
SERVER_NAME='autenticazione.lavoripubblici.sicilia.it'
CERTBOT_EMAIL='admin@mwg.it'
DOCUMENT_ROOT='/var/www/html'
MARIADB_USERNAME='mwg'
MARIADB_PASSWORD='Aw42#1e_w.as'
MARIADB_DATABASE='autenticazione'
FOLDER="$(dirname $(realpath $0))"
apt update
apt -y dist-upgrade
apt install -y apache2 certbot cron libapache2-mod-auth-openidc mariadb-server php php-mysql python
# OpenSSL
sed -i -e 's/^CipherString = .*/CipherString = DEFAULT@SECLEVEL=1/' /etc/ssl/openssl.cnf
# MariaDB
# mysql -e "DROP DATABASE IF EXISTS \`$MARIADB_DATABASE\`;"
if [ "0" = "$(mysql -B -N -e "SHOW DATABASES LIKE '$MARIADB_DATABASE';" | wc -l)" ]; then
mysql < $FOLDER/file/autenticazione.sql
fi
if [ "0" = "$(mysql -B -N -e \
"SELECT \`User\` FROM \`mysql\`.\`user\` WHERE \`User\`='$MARIADB_USERNAME' AND \`Host\`='localhost';" | \
wc -l)" ]; then
mysql -e "GRANT ALL PRIVILEGES ON \`$MARIADB_DATABASE\`.* TO '$MARIADB_USERNAME'@'localhost' IDENTIFIED BY \
'$MARIADB_PASSWORD'"
fi
sed -i -e "s/\\(\\s*'password'\\s*=>\\s*\\).*/\\1'$MARIADB_PASSWORD',/" /var/www/App/Config/database.php
# Sshd
mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/mwg.conf
# Autenticazione
cp $FOLDER/usr-local-bin/* /usr/local/bin
. /usr/local/bin/project_env.sh
echo "export SERVER_NAME=$SERVER_NAME" >> /usr/local/bin/project_env.sh
echo "export APPLICATION_URL=https://$SERVER_NAME:443" >> /usr/local/bin/project_env.sh
# Apache
cp $FOLDER/file/ssl-params.conf /etc/apache2/conf-available/
if [ "$(grep /usr/local/bin/project_env.sh /etc/apache2/envvars | wc -l)" = "0" ]; then
echo '. /usr/local/bin/project_env.sh' >> /etc/apache2/envvars
fi
a2enconf ssl-params
a2enmod headers
a2enmod rewrite
a2enmod ssl
cd /etc/apache2/sites-enabled
find /etc/apache2/sites-enabled -type l -exec sh -c 'a2dissite $(basename {})' \;
cp $FOLDER/file/0certbot.conf /etc/apache2/sites-available
a2ensite 0certbot
systemctl restart apache2
certbot certonly -d $SERVER_NAME --webroot --webroot-path /var/www/certbot -m $CERTBOT_EMAIL --agree-tos \
--non-interactive --no-eff-email --expand
mkdir -p /etc/apache2/keys
cp $FOLDER/metadata/private.pem $FOLDER/metadata/public.pem /etc/apache2/keys
cp $FOLDER/file/1autenticazione.conf /etc/apache2/sites-available
a2ensite 1autenticazione
/usr/local/bin/auto-update-gov-certificates
# Cronjob
cp $FOLDER/cron/* /etc/cron.d
echo Installazione terminata
# apt purge --auto-remove apache2 certbot cron libapache2-mod-auth-openidc mariadb-server php php-mysql python;rmdir /var/lib/apache2/;rm -Rf /etc/ssl/itaca/
# openssl req -key private.pem -new -x509 -days 3650 -subj /C=IT/ST=Sicilia/O=Regione Siciliana/OU=Autenticazione Lavori Pubblici/CN=Autenticazione Lavori Pubblici -out cert.pem
# openssl pkcs12 -export -inkey private.pem -in cert.pem -out keys.pfx -name autenticazione
# keytool -v -list -keystore keys.pfx

39
setup/usr-local-bin/auto-update-gov-certificates

@ -0,0 +1,39 @@
#!/bin/sh -e
LOG_FILE="/var/log/auto-update-gov-certificates.log"
echo "$(date "+%FT%T") Start auto upgrade Gov Certificates..." >> "$LOG_FILE"
. /usr/local/bin/project_env.sh
SIM_DIR="/etc/ssl/itaca"
touch "$LOG_FILE"
TMP_CERT_PATH=`mktemp -d`
echo "$(date "+%FT%T") Remove temporary certificates file from $TMP_CERT_PATH" >> "$LOG_FILE"
rm -Rf "$TMP_CERT_PATH"/* >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") Downloading Gov Certificates in tmp path $TMP_CERT_PATH..." >> "$LOG_FILE"
/usr/local/bin/parse-gov-certs.py --output-folder "$TMP_CERT_PATH" --service-type-identifier "$GOV_TRUST_CERTS_SERVICE_TYPE_IDENTIFIER" >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") Downloading Gov Certificates...[END]" >> "$LOG_FILE"
echo "$(date "+%FT%T") Save Gov Certificates into $GOV_TRUST_CERTS_OUTPUT_PATH" >> "$LOG_FILE"
mkdir -p "$GOV_TRUST_CERTS_OUTPUT_PATH" >> "$LOG_FILE" 2>&1
rm -Rf "$GOV_TRUST_CERTS_OUTPUT_PATH"/* >> "$LOG_FILE" 2>&1
mv "$TMP_CERT_PATH"/* "$GOV_TRUST_CERTS_OUTPUT_PATH" >> "$LOG_FILE" 2>&1
rm -Rf "$TMP_CERT_PATH" >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") Removing simlinks from $SIM_DIR ..." >> "$LOG_FILE"
mkdir -p "$SIM_DIR" >> "$LOG_FILE" 2>&1
rm -Rf "$SIM_DIR"/* >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") Creating simlinks into $SIM_DIR/" >> "$LOG_FILE"
find "$GOV_TRUST_CERTS_OUTPUT_PATH" -type f | xargs -I{} basename "{}" | xargs -I{} ln -s "$GOV_TRUST_CERTS_OUTPUT_PATH/{}" "$SIM_DIR/{}" >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") Re-Hashing $SIM_DIR/..." >> "$LOG_FILE"
c_rehash "$SIM_DIR/" >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") Start auto upgrade Gov Certificates...[END]" >> "$LOG_FILE"
echo "$(date "+%FT%T") Restart Apache HTTP Service..." >> "$LOG_FILE"
systemctl restart apache2 >> "$LOG_FILE" 2>&1
echo "$(date "+%FT%T") End auto upgrade Gov Certificates..." >> "$LOG_FILE"

146
setup/usr-local-bin/parse-gov-certs.py

@ -0,0 +1,146 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) Marco Trevisan
#
# Authors:
# Marco Trevisan <marco@trevisan.xyz>
#
# Revision for new URL:
# Andrea Costantino <costan@amg.it>
#
# Revision for new XPath Query (add a filter by service type identifier)
# Antonio Musarra <antonio.musarra@gmail.com>
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUTa
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Get Italian government Certification Authority certificates from used by
# by various National Service SmartCards (Carta Nazionale dei Servizi- CNS)
#
# Original URI:
# - http://www.agid.gov.it/agenda-digitale/infrastrutture-architetture/firme-elettroniche/certificati
#
# Current XML file:
# - https://eidas.agid.gov.it/TL/TSL-IT.xml
import argparse
import re
import sys
import xml.etree.ElementTree as ET
import textwrap
import os
DEFAULT_XML_URI = "https://eidas.agid.gov.it/TL/TSL-IT.xml"
EXTENSION = ".pem"
def get_certs_xml():
if sys.version_info[0] == 2:
import urllib2
request = urllib2
else:
import urllib.request
request = urllib.request
return request.urlopen(DEFAULT_XML_URI)
def write_certificate(f, x509_cert):
f.write('-----BEGIN CERTIFICATE-----\n')
for line in textwrap.wrap(x509_cert, 65):
f.write(line+'\n')
f.write('-----END CERTIFICATE-----\n')
def get_service_info(service):
name = service.find("*/"+ns+"Name").text
x509_cert = service.find("*//"+ns+"X509Certificate").text
return {'name': name, 'x509_cert': x509_cert}
parser = argparse.ArgumentParser()
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--output-folder", help="Where to save the certs files")
action.add_argument("--output-file", help="File saving certificates")
parser.add_argument("--cert-file", help="Input Xml file, instead of %s" % DEFAULT_XML_URI)
parser.add_argument("--service-type-identifier", help="Save certs by Service Type Identifier, instead of all")
args = parser.parse_args()
if args.output_folder:
if os.path.exists(args.output_folder):
if not os.path.isdir(args.output_folder):
print("Impossible to save certificates in `%s': file exists and is not a folder." % args.output_folder)
sys.exit(1)
else:
os.makedirs(args.output_folder)
elif args.output_file:
if os.path.exists(args.output_file):
if not os.path.isfile(args.output_file):
print("Impossible to write on `%s', it's not a file." % args.output_file)
sys.exit(1)
print("File `%s' will be overwritten..." % args.output_file)
if args.cert_file:
tree = ET.parse(args.cert_file)
root = tree.getroot()
else:
root = ET.fromstring(get_certs_xml().read())
try:
[ns] = re.findall("({[^}]*}).*", root.tag)
except:
ns = ""
print("Namespace: `%s`", ns)
if args.service_type_identifier:
services = root.findall(ns+"TrustServiceProviderList//"+ns+"TSPService/"+ns+"ServiceInformation["+ns+"ServiceTypeIdentifier='"+args.service_type_identifier+"']")
else:
services = root.findall(ns+"TrustServiceProviderList//"+ns+"TSPService/"+ns+"ServiceInformation")
if args.output_folder:
for service in services:
try:
info = get_service_info(service)
name = re.sub('[A-z]{1,2}=', '_', re.sub('[/\,\' "]', '_', info['name'])).replace('__', '_').strip('_- ')
filename = args.output_folder+os.path.sep+name
idx = 1
tmpname = filename
while os.path.exists(tmpname+EXTENSION):
tmpname = filename+str(idx)
idx += 1
filename = tmpname+EXTENSION
f = open(filename, 'w')
write_certificate(f, info['x509_cert'])
f.close()
print("Added certificate: %s" % filename)
except Exception as e:
print("Impossible to add file: %s" % e)
pass
else:
f = open(args.output_file, 'w')
for service in services:
try:
info = get_service_info(service)
write_certificate(f, info['x509_cert'])
print("Added certificate %s" % info['name'])
except Exception as e:
print("Impossible to add certificate to file: %s" % e)
pass
f.close()

12
setup/usr-local-bin/project_env.sh

@ -0,0 +1,12 @@
export APACHE_SSL_VERIFY_CLIENT=optional
export APACHE_LOG_LEVEL=info
export GOV_TRUST_CERTS_OUTPUT_PATH=/usr/share/ca-certificates/it-gov
export APACHE_SSL_LOG_LEVEL=debug
export APACHE_SSL_PORT=443
export APACHE_SSL_PRIVATE=privkey.pem
export GOV_TRUST_CERTS_SERVICE_TYPE_IDENTIFIER=http://uri.etsi.org/TrstSvc/Svctype/IdV
export APACHE_SSL_CERTS=fullchain.pem
export CLIENT_VERIFY_LANDING_PAGE=/errors/no-cns.php
export APACHE_SERVER_ADMIN=info@lavoripubblici.sicilia.it
export APACHE_RUN_USER=mwg
export APACHE_RUN_GROUP=mwg
Caricamento…
Annulla
Salva