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
0ca3c8e
feat: implement TSA
vitormattos Sep 9, 2025
42cf718
fix: undefined tst
vitormattos Sep 9, 2025
d09296a
fix: typo
vitormattos Sep 22, 2025
7dd1078
feat: make possible customize URL and TSA Policy OID
vitormattos Oct 14, 2025
313ba33
feat: send TSA Policy OID to JSignPdf
vitormattos Oct 14, 2025
cf32f6e
fix: psalm issues
vitormattos Oct 14, 2025
f15b012
fix: psalm issue
vitormattos Oct 14, 2025
00f813e
feat: make possible use authentication at TSA
vitormattos Oct 14, 2025
e4502be
feat: send to JSignPdf the TSA settings
vitormattos Oct 14, 2025
9fd8e24
chore: save TSA settings
vitormattos Oct 14, 2025
ca8f86f
chore: implement tests to TSA
vitormattos Oct 14, 2025
6429048
chore: review implementation of TSA Authentication
vitormattos Oct 14, 2025
3431046
fix: cs
vitormattos Oct 14, 2025
e491f10
fix: psalm issue
vitormattos Oct 14, 2025
3922a43
chore: update openapi documentation
vitormattos Oct 14, 2025
73eaed1
fix: remove tests out of scope of contract test
vitormattos Oct 14, 2025
d434175
fix: remove OID when this value is empty
vitormattos Oct 14, 2025
34a4e7d
chore: remove requirement of CSRF
vitormattos Oct 14, 2025
ee2bff6
feat: cover with unit tests
vitormattos Oct 14, 2025
8c30b90
fix: change after implement unit test
vitormattos Oct 14, 2025
926dcfa
chore: change the example by a real value
vitormattos Oct 14, 2025
8b82cc2
feat: implement integration tests
vitormattos Oct 14, 2025
b7fca90
chore: code optimized
vitormattos Oct 14, 2025
cac434d
fix: changes after refactor
vitormattos Oct 14, 2025
f438326
fix: psalm issues
vitormattos Oct 14, 2025
ff286bd
chore: improve the return from TSA parser
vitormattos Oct 15, 2025
a4341d2
chore: optimize code
vitormattos Oct 15, 2025
69eccaa
feat: display more TSA data
vitormattos Oct 15, 2025
17335ee
chore: remove unecessary code
vitormattos Oct 15, 2025
b13e9f7
chore: remove unecessary row
vitormattos Oct 15, 2025
cb43454
chore: remove unused step
vitormattos Oct 15, 2025
69a11d9
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 @@ -406,18 +406,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 @@ -426,8 +477,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 @@ -99,18 +99,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