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
663843c
feat: implement TSA
vitormattos Sep 9, 2025
1f39668
fix: undefined tst
vitormattos Sep 9, 2025
f4950b5
fix: typo
vitormattos Sep 22, 2025
842c869
feat: make possible customize URL and TSA Policy OID
vitormattos Oct 14, 2025
756ddda
feat: send TSA Policy OID to JSignPdf
vitormattos Oct 14, 2025
7a0a661
fix: psalm issues
vitormattos Oct 14, 2025
4446dd1
fix: psalm issue
vitormattos Oct 14, 2025
4686fd4
feat: make possible use authentication at TSA
vitormattos Oct 14, 2025
79b7fb6
feat: send to JSignPdf the TSA settings
vitormattos Oct 14, 2025
c2ef7b4
chore: save TSA settings
vitormattos Oct 14, 2025
3bde626
chore: implement tests to TSA
vitormattos Oct 14, 2025
3828800
chore: review implementation of TSA Authentication
vitormattos Oct 14, 2025
94f1ff0
fix: cs
vitormattos Oct 14, 2025
173826a
fix: psalm issue
vitormattos Oct 14, 2025
56b38e9
chore: update openapi documentation
vitormattos Oct 14, 2025
3d76875
fix: remove tests out of scope of contract test
vitormattos Oct 14, 2025
08ebfb3
fix: remove OID when this value is empty
vitormattos Oct 14, 2025
9d8b01d
chore: remove requirement of CSRF
vitormattos Oct 14, 2025
1f10f57
feat: cover with unit tests
vitormattos Oct 14, 2025
944880a
fix: change after implement unit test
vitormattos Oct 14, 2025
464b21b
chore: change the example by a real value
vitormattos Oct 14, 2025
52147fe
feat: implement integration tests
vitormattos Oct 14, 2025
2ea5439
chore: code optimized
vitormattos Oct 14, 2025
46a0556
fix: changes after refactor
vitormattos Oct 14, 2025
6ce2b38
fix: psalm issues
vitormattos Oct 14, 2025
d6c88a7
chore: improve the return from TSA parser
vitormattos Oct 15, 2025
8140d1c
chore: optimize code
vitormattos Oct 15, 2025
2a2a1c4
feat: display more TSA data
vitormattos Oct 15, 2025
15f6d10
chore: remove unecessary code
vitormattos Oct 15, 2025
995307c
chore: remove unecessary row
vitormattos Oct 15, 2025
5fc2e00
chore: remove unused step
vitormattos Oct 15, 2025
1a979c1
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