Skip to content

Commit ecc3a0f

Browse files
authored
Merge pull request #5582 from LibreSign/backport/5423/stable32
[stable32] feat: implement TSA
2 parents 32aa730 + 685d91c commit ecc3a0f

File tree

18 files changed

+2325
-15
lines changed

18 files changed

+2325
-15
lines changed

lib/Controller/AdminController.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use OCA\Libresign\Service\ReminderService;
2323
use OCA\Libresign\Service\SignatureBackgroundService;
2424
use OCA\Libresign\Service\SignatureTextService;
25+
use OCA\Libresign\Settings\Admin;
2526
use OCP\AppFramework\Http;
2627
use OCP\AppFramework\Http\Attribute\ApiRoute;
2728
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
@@ -662,4 +663,117 @@ public function reminderSave(
662663
}
663664
return new DataResponse($response);
664665
}
666+
667+
/**
668+
* Set TSA configuration values with proper sensitive data handling
669+
*
670+
* Only saves configuration if tsa_url is provided. Automatically manages
671+
* username/password fields based on authentication type.
672+
*
673+
* @param string|null $tsa_url TSA server URL (required for saving)
674+
* @param string|null $tsa_policy_oid TSA policy OID
675+
* @param string|null $tsa_auth_type Authentication type (none|basic), defaults to 'none'
676+
* @param string|null $tsa_username Username for basic authentication
677+
* @param string|null $tsa_password Password for basic authentication (stored as sensitive data)
678+
* @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{status: 'error', message: string}, array{}>
679+
*
680+
* 200: OK
681+
* 400: Validation error
682+
*/
683+
#[NoCSRFRequired]
684+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
685+
public function setTsaConfig(
686+
?string $tsa_url = null,
687+
?string $tsa_policy_oid = null,
688+
?string $tsa_auth_type = null,
689+
?string $tsa_username = null,
690+
?string $tsa_password = null,
691+
): DataResponse {
692+
if (empty($tsa_url)) {
693+
return $this->deleteTsaConfig();
694+
}
695+
696+
$trimmedUrl = trim($tsa_url);
697+
if (!filter_var($trimmedUrl, FILTER_VALIDATE_URL)
698+
|| !in_array(parse_url($trimmedUrl, PHP_URL_SCHEME), ['http', 'https'])) {
699+
return new DataResponse([
700+
'status' => 'error',
701+
'message' => 'Invalid URL format'
702+
], Http::STATUS_BAD_REQUEST);
703+
}
704+
705+
$this->appConfig->setValueString(Application::APP_ID, 'tsa_url', $trimmedUrl);
706+
707+
if (empty($tsa_policy_oid)) {
708+
$this->appConfig->deleteKey(Application::APP_ID, 'tsa_policy_oid');
709+
} else {
710+
$trimmedOid = trim($tsa_policy_oid);
711+
if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $trimmedOid)) {
712+
return new DataResponse([
713+
'status' => 'error',
714+
'message' => 'Invalid OID format'
715+
], Http::STATUS_BAD_REQUEST);
716+
}
717+
$this->appConfig->setValueString(Application::APP_ID, 'tsa_policy_oid', $trimmedOid);
718+
}
719+
720+
$authType = $tsa_auth_type ?? 'none';
721+
$this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
722+
723+
if ($authType === 'basic') {
724+
$hasUsername = !empty($tsa_username);
725+
$hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
726+
727+
if (!$hasUsername && !$hasPassword) {
728+
return new DataResponse([
729+
'status' => 'error',
730+
'message' => 'Username and password are required for basic authentication'
731+
], Http::STATUS_BAD_REQUEST);
732+
} elseif (!$hasUsername) {
733+
return new DataResponse([
734+
'status' => 'error',
735+
'message' => 'Username is required'
736+
], Http::STATUS_BAD_REQUEST);
737+
} elseif (!$hasPassword) {
738+
return new DataResponse([
739+
'status' => 'error',
740+
'message' => 'Password is required'
741+
], Http::STATUS_BAD_REQUEST);
742+
}
743+
744+
$this->appConfig->setValueString(Application::APP_ID, 'tsa_username', trim($tsa_username));
745+
$this->appConfig->setValueString(
746+
Application::APP_ID,
747+
key: 'tsa_password',
748+
value: $tsa_password,
749+
sensitive: true,
750+
);
751+
} else {
752+
$this->appConfig->deleteKey(Application::APP_ID, 'tsa_username');
753+
$this->appConfig->deleteKey(Application::APP_ID, 'tsa_password');
754+
}
755+
756+
return new DataResponse(['status' => 'success']);
757+
}
758+
759+
/**
760+
* Delete TSA configuration
761+
*
762+
* Delete all TSA configuration fields from the application settings.
763+
*
764+
* @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
765+
*
766+
* 200: OK
767+
*/
768+
#[NoCSRFRequired]
769+
#[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
770+
public function deleteTsaConfig(): DataResponse {
771+
$fields = ['tsa_url', 'tsa_policy_oid', 'tsa_auth_type', 'tsa_username', 'tsa_password'];
772+
773+
foreach ($fields as $field) {
774+
$this->appConfig->deleteKey(Application::APP_ID, $field);
775+
}
776+
777+
return new DataResponse(['status' => 'success']);
778+
}
665779
}

lib/Handler/SignEngine/JSignPdfHandler.php

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,18 +403,69 @@ private function listParamsToString(array $params): string {
403403
return $paramString;
404404
}
405405

406+
private function getTsaParameters(): array {
407+
$tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', '');
408+
if (empty($tsaUrl)) {
409+
return [];
410+
}
411+
412+
$params = [
413+
'--tsa-server-url' => $tsaUrl,
414+
'--tsa-policy-oid' => $this->appConfig->getValueString(Application::APP_ID, 'tsa_policy_oid', ''),
415+
];
416+
417+
if (!$params['--tsa-policy-oid']) {
418+
unset($params['--tsa-policy-oid']);
419+
}
420+
421+
$tsaAuthType = $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none');
422+
if ($tsaAuthType === 'basic') {
423+
$tsaUsername = $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', '');
424+
$tsaPassword = $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', '');
425+
426+
if (!empty($tsaUsername) && !empty($tsaPassword)) {
427+
$params['--tsa-authentication'] = 'PASSWORD';
428+
$params['--tsa-user'] = $tsaUsername;
429+
$params['--tsa-password'] = $tsaPassword;
430+
}
431+
}
432+
433+
return $params;
434+
}
435+
406436
private function signWrapper(JSignPDF $jSignPDF): string {
407437
try {
438+
$params = [
439+
'--hash-algorithm' => $this->getHashAlgorithm(),
440+
];
441+
442+
$params = array_merge($params, $this->getTsaParameters());
408443
$param = $this->getJSignParam();
409444
$param
410445
->setJSignParameters(
411446
$this->jSignParam->getJSignParameters()
412-
. ' --hash-algorithm ' . $this->getHashAlgorithm()
447+
. $this->listParamsToString($params)
413448
);
414449
$jSignPDF->setParam($param);
415450
return $jSignPDF->sign();
416451
} catch (\Throwable $th) {
417-
$rows = str_getcsv($th->getMessage());
452+
$errorMessage = $th->getMessage();
453+
$rows = str_getcsv($errorMessage);
454+
455+
$tsaError = array_filter($rows, fn ($r) => str_contains((string)$r, 'Invalid TSA'));
456+
if (!empty($tsaError)) {
457+
$tsaErrorMsg = current($tsaError);
458+
if (preg_match("/Invalid TSA '([^']+)'/", $tsaErrorMsg, $matches)) {
459+
$friendlyMessage = 'Timestamp Authority (TSA) service is unavailable or misconfigured.' . "\n"
460+
. 'Please check the TSA configuration.';
461+
} else {
462+
$friendlyMessage = 'Timestamp Authority (TSA) service error.' . "\n"
463+
. 'Please check the TSA configuration.';
464+
}
465+
throw new LibresignException($friendlyMessage);
466+
}
467+
468+
// Check for hash algorithm errors
418469
$hashAlgorithm = array_filter($rows, fn ($r) => str_contains((string)$r, 'The chosen hash algorithm'));
419470
if (!empty($hashAlgorithm)) {
420471
$hashAlgorithm = current($hashAlgorithm);
@@ -423,8 +474,9 @@ private function signWrapper(JSignPDF $jSignPDF): string {
423474
$hashAlgorithm = preg_replace('/\.( )/', ".\n", $hashAlgorithm);
424475
throw new LibresignException($hashAlgorithm);
425476
}
426-
$this->logger->error('Error at JSignPdf side. LibreSign can not do nothing. Follow the error message: ' . $th->getMessage());
427-
throw new \Exception($th->getMessage());
477+
478+
$this->logger->error('Error at JSignPdf side. LibreSign can not do nothing. Follow the error message: ' . $errorMessage);
479+
throw new \Exception($errorMessage);
428480
}
429481
}
430482
}

lib/Handler/SignEngine/Pkcs12Handler.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,18 @@ public function getCertificateChain($resource): array {
9898
continue;
9999
}
100100

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

115115
$pkcs7PemSignature = $this->der2pem($signature);

0 commit comments

Comments
 (0)