Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5ad76b1
feat: implement TSA
vitormattos Sep 9, 2025
ddcbec5
fix: undefined tst
vitormattos Sep 9, 2025
1c97b40
fix: typo
vitormattos Sep 22, 2025
bcdbfcc
feat: make possible customize URL and TSA Policy OID
vitormattos Oct 14, 2025
c25540f
feat: send TSA Policy OID to JSignPdf
vitormattos Oct 14, 2025
d01c70c
fix: psalm issues
vitormattos Oct 14, 2025
4b0a3ae
fix: psalm issue
vitormattos Oct 14, 2025
8eced3b
feat: make possible use authentication at TSA
vitormattos Oct 14, 2025
6e71556
feat: send to JSignPdf the TSA settings
vitormattos Oct 14, 2025
91bd653
chore: save TSA settings
vitormattos Oct 14, 2025
4f0f789
chore: implement tests to TSA
vitormattos Oct 14, 2025
b6fdfb6
chore: review implementation of TSA Authentication
vitormattos Oct 14, 2025
c7d1853
fix: cs
vitormattos Oct 14, 2025
0b54c33
fix: psalm issue
vitormattos Oct 14, 2025
b5252a2
chore: update openapi documentation
vitormattos Oct 14, 2025
ee7f958
fix: remove tests out of scope of contract test
vitormattos Oct 14, 2025
d670987
fix: remove OID when this value is empty
vitormattos Oct 14, 2025
bf3c420
chore: remove requirement of CSRF
vitormattos Oct 14, 2025
4f42f47
feat: cover with unit tests
vitormattos Oct 14, 2025
069d677
fix: change after implement unit test
vitormattos Oct 14, 2025
d8a1e45
chore: change the example by a real value
vitormattos Oct 14, 2025
00890c2
feat: implement integration tests
vitormattos Oct 14, 2025
1c308a3
chore: code optimized
vitormattos Oct 14, 2025
218e884
fix: changes after refactor
vitormattos Oct 14, 2025
86d61e2
fix: psalm issues
vitormattos Oct 14, 2025
806974a
chore: improve the return from TSA parser
vitormattos Oct 15, 2025
7a7ef36
chore: optimize code
vitormattos Oct 15, 2025
d2e82a6
feat: display more TSA data
vitormattos Oct 15, 2025
41766fd
chore: remove unecessary code
vitormattos Oct 15, 2025
9bae85c
chore: remove unecessary row
vitormattos Oct 15, 2025
4ebe7ee
chore: remove unused step
vitormattos Oct 15, 2025
685d91c
fix: handle error about TSA
vitormattos Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions lib/Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use OCA\Libresign\Service\ReminderService;
use OCA\Libresign\Service\SignatureBackgroundService;
use OCA\Libresign\Service\SignatureTextService;
use OCA\Libresign\Settings\Admin;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
Expand Down Expand Up @@ -662,4 +663,117 @@ public function reminderSave(
}
return new DataResponse($response);
}

/**
* Set TSA configuration values with proper sensitive data handling
*
* Only saves configuration if tsa_url is provided. Automatically manages
* username/password fields based on authentication type.
*
* @param string|null $tsa_url TSA server URL (required for saving)
* @param string|null $tsa_policy_oid TSA policy OID
* @param string|null $tsa_auth_type Authentication type (none|basic), defaults to 'none'
* @param string|null $tsa_username Username for basic authentication
* @param string|null $tsa_password Password for basic authentication (stored as sensitive data)
* @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{status: 'error', message: string}, array{}>
*
* 200: OK
* 400: Validation error
*/
#[NoCSRFRequired]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
public function setTsaConfig(
?string $tsa_url = null,
?string $tsa_policy_oid = null,
?string $tsa_auth_type = null,
?string $tsa_username = null,
?string $tsa_password = null,
): DataResponse {
if (empty($tsa_url)) {
return $this->deleteTsaConfig();
}

$trimmedUrl = trim($tsa_url);
if (!filter_var($trimmedUrl, FILTER_VALIDATE_URL)
|| !in_array(parse_url($trimmedUrl, PHP_URL_SCHEME), ['http', 'https'])) {
return new DataResponse([
'status' => 'error',
'message' => 'Invalid URL format'
], Http::STATUS_BAD_REQUEST);
}

$this->appConfig->setValueString(Application::APP_ID, 'tsa_url', $trimmedUrl);

if (empty($tsa_policy_oid)) {
$this->appConfig->deleteKey(Application::APP_ID, 'tsa_policy_oid');
} else {
$trimmedOid = trim($tsa_policy_oid);
if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $trimmedOid)) {
return new DataResponse([
'status' => 'error',
'message' => 'Invalid OID format'
], Http::STATUS_BAD_REQUEST);
}
$this->appConfig->setValueString(Application::APP_ID, 'tsa_policy_oid', $trimmedOid);
}

$authType = $tsa_auth_type ?? 'none';
$this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);

if ($authType === 'basic') {
$hasUsername = !empty($tsa_username);
$hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;

if (!$hasUsername && !$hasPassword) {
return new DataResponse([
'status' => 'error',
'message' => 'Username and password are required for basic authentication'
], Http::STATUS_BAD_REQUEST);
} elseif (!$hasUsername) {
return new DataResponse([
'status' => 'error',
'message' => 'Username is required'
], Http::STATUS_BAD_REQUEST);
} elseif (!$hasPassword) {
return new DataResponse([
'status' => 'error',
'message' => 'Password is required'
], Http::STATUS_BAD_REQUEST);
}

$this->appConfig->setValueString(Application::APP_ID, 'tsa_username', trim($tsa_username));
$this->appConfig->setValueString(
Application::APP_ID,
key: 'tsa_password',
value: $tsa_password,
sensitive: true,
);
} else {
$this->appConfig->deleteKey(Application::APP_ID, 'tsa_username');
$this->appConfig->deleteKey(Application::APP_ID, 'tsa_password');
}

return new DataResponse(['status' => 'success']);
}

/**
* Delete TSA configuration
*
* Delete all TSA configuration fields from the application settings.
*
* @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
*
* 200: OK
*/
#[NoCSRFRequired]
#[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
public function deleteTsaConfig(): DataResponse {
$fields = ['tsa_url', 'tsa_policy_oid', 'tsa_auth_type', 'tsa_username', 'tsa_password'];

foreach ($fields as $field) {
$this->appConfig->deleteKey(Application::APP_ID, $field);
}

return new DataResponse(['status' => 'success']);
}
}
60 changes: 56 additions & 4 deletions lib/Handler/SignEngine/JSignPdfHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,18 +403,69 @@ private function listParamsToString(array $params): string {
return $paramString;
}

private function getTsaParameters(): array {
$tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', '');
if (empty($tsaUrl)) {
return [];
}

$params = [
'--tsa-server-url' => $tsaUrl,
'--tsa-policy-oid' => $this->appConfig->getValueString(Application::APP_ID, 'tsa_policy_oid', ''),
];

if (!$params['--tsa-policy-oid']) {
unset($params['--tsa-policy-oid']);
}

$tsaAuthType = $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none');
if ($tsaAuthType === 'basic') {
$tsaUsername = $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', '');
$tsaPassword = $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', '');

if (!empty($tsaUsername) && !empty($tsaPassword)) {
$params['--tsa-authentication'] = 'PASSWORD';
$params['--tsa-user'] = $tsaUsername;
$params['--tsa-password'] = $tsaPassword;
}
}

return $params;
}

private function signWrapper(JSignPDF $jSignPDF): string {
try {
$params = [
'--hash-algorithm' => $this->getHashAlgorithm(),
];

$params = array_merge($params, $this->getTsaParameters());
$param = $this->getJSignParam();
$param
->setJSignParameters(
$this->jSignParam->getJSignParameters()
. ' --hash-algorithm ' . $this->getHashAlgorithm()
. $this->listParamsToString($params)
);
$jSignPDF->setParam($param);
return $jSignPDF->sign();
} catch (\Throwable $th) {
$rows = str_getcsv($th->getMessage());
$errorMessage = $th->getMessage();
$rows = str_getcsv($errorMessage);

$tsaError = array_filter($rows, fn ($r) => str_contains((string)$r, 'Invalid TSA'));
if (!empty($tsaError)) {
$tsaErrorMsg = current($tsaError);
if (preg_match("/Invalid TSA '([^']+)'/", $tsaErrorMsg, $matches)) {
$friendlyMessage = 'Timestamp Authority (TSA) service is unavailable or misconfigured.' . "\n"
. 'Please check the TSA configuration.';
} else {
$friendlyMessage = 'Timestamp Authority (TSA) service error.' . "\n"
. 'Please check the TSA configuration.';
}
throw new LibresignException($friendlyMessage);
}

// Check for hash algorithm errors
$hashAlgorithm = array_filter($rows, fn ($r) => str_contains((string)$r, 'The chosen hash algorithm'));
if (!empty($hashAlgorithm)) {
$hashAlgorithm = current($hashAlgorithm);
Expand All @@ -423,8 +474,9 @@ private function signWrapper(JSignPDF $jSignPDF): string {
$hashAlgorithm = preg_replace('/\.( )/', ".\n", $hashAlgorithm);
throw new LibresignException($hashAlgorithm);
}
$this->logger->error('Error at JSignPdf side. LibreSign can not do nothing. Follow the error message: ' . $th->getMessage());
throw new \Exception($th->getMessage());

$this->logger->error('Error at JSignPdf side. LibreSign can not do nothing. Follow the error message: ' . $errorMessage);
throw new \Exception($errorMessage);
}
}
}
22 changes: 11 additions & 11 deletions lib/Handler/SignEngine/Pkcs12Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,18 @@ public function getCertificateChain($resource): array {
continue;
}

if (!isset($fromFallback['signingTime'])) {
// Probably the best way to do this would be:
// ASN1::asn1map($decoded[0], Maps\TheMapName::MAP);
// But, what's the MAP to use?
//
// With maps also could be possible read all certificate data and
// maybe discart openssl at this pint
try {
$decoded = ASN1::decodeBER($signature);
$certificates[$signerCounter]['signingTime'] = $decoded[0]['content'][1]['content'][0]['content'][4]['content'][0]['content'][3]['content'][1]['content'][1]['content'][0]['content'];
} catch (\Throwable) {
$tsa = new TSA();
$decoded = ASN1::decodeBER($signature);
try {
$timestampData = $tsa->extract($decoded);
if (!empty($timestampData['genTime']) || !empty($timestampData['policy']) || !empty($timestampData['serialNumber'])) {
$certificates[$signerCounter]['timestamp'] = $timestampData;
}
} catch (\Throwable $e) {
}

if (!isset($fromFallback['signingTime']) || !$fromFallback['signingTime'] instanceof \DateTime) {
$certificates[$signerCounter]['signingTime'] = $tsa->getSigninTime($decoded);
}

$pkcs7PemSignature = $this->der2pem($signature);
Expand Down
Loading
Loading