Skip to content

Commit 2f3df72

Browse files
authored
Merge pull request #5724 from LibreSign/backport/5722/stable31
[stable31] feat: return more data from validation endpoint
2 parents fc23f03 + 0b04989 commit 2f3df72

File tree

4 files changed

+192
-73
lines changed

4 files changed

+192
-73
lines changed

lib/Service/FileService.php

Lines changed: 54 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -438,84 +438,75 @@ private function loadLibreSignSigners(): void {
438438
private function loadSignersFromCertData(): void {
439439
$this->loadCertDataFromLibreSignFile();
440440
foreach ($this->certData as $index => $signer) {
441-
if (!empty($signer['chain'][0]['name'])) {
442-
$this->fileData->signers[$index]['subject'] = $signer['chain'][0]['name'];
443-
}
444-
if (!empty($signer['chain'][0]['field'])) {
445-
$this->fileData->signers[$index]['field'] = $signer['chain'][0]['field'];
446-
}
447-
if (!empty($signer['chain'][0]['validFrom_time_t'])) {
448-
$this->fileData->signers[$index]['valid_from'] = (new DateTime('@' . $signer['chain'][0]['validFrom_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM);
449-
}
450-
if (!empty($signer['chain'][0]['validTo_time_t'])) {
451-
$this->fileData->signers[$index]['valid_to'] = (new DateTime('@' . $signer['chain'][0]['validTo_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM);
441+
if (isset($signer['timestamp'])) {
442+
$this->fileData->signers[$index]['timestamp'] = $signer['timestamp'];
443+
if (isset($signer['timestamp']['genTime']) && $signer['timestamp']['genTime'] instanceof DateTimeInterface) {
444+
$this->fileData->signers[$index]['timestamp']['genTime'] = $signer['timestamp']['genTime']->format(DateTimeInterface::ATOM);
445+
}
452446
}
453-
if (!empty($signer['signingTime'])) {
447+
if (isset($signer['signingTime']) && $signer['signingTime'] instanceof DateTimeInterface) {
448+
$this->fileData->signers[$index]['signingTime'] = $signer['signingTime'];
454449
$this->fileData->signers[$index]['signed'] = $signer['signingTime']->format(DateTimeInterface::ATOM);
455450
}
456-
$this->fileData->signers[$index]['signature_validation'] = $signer['chain'][0]['signature_validation'];
457-
if (!empty($signer['chain'][0]['certificate_validation'])) {
458-
$this->fileData->signers[$index]['certificate_validation'] = $signer['chain'][0]['certificate_validation'];
459-
}
460-
if (!empty($signer['chain'][0]['signatureTypeSN'])) {
461-
$this->fileData->signers[$index]['hash_algorithm'] = $signer['chain'][0]['signatureTypeSN'];
451+
foreach ($signer['chain'] as $chainIndex => $chainItem) {
452+
$chainArr = $chainItem;
453+
if (isset($chainItem['validFrom_time_t']) && is_numeric($chainItem['validFrom_time_t'])) {
454+
$chainArr['valid_from'] = (new DateTime('@' . $chainItem['validFrom_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM);
455+
}
456+
if (isset($chainItem['validTo_time_t']) && is_numeric($chainItem['validTo_time_t'])) {
457+
$chainArr['valid_to'] = (new DateTime('@' . $chainItem['validTo_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM);
458+
}
459+
$chainArr['displayName'] = $chainArr['name'] ?? ($chainArr['subject']['CN'] ?? '');
460+
$this->fileData->signers[$index]['chain'][$chainIndex] = $chainArr;
461+
if ($chainIndex === 0) {
462+
$this->fileData->signers[$index] = array_merge($chainArr, $this->fileData->signers[$index] ?? []);
463+
$this->fileData->signers[$index]['uid'] = $this->resolveUid($chainArr);
464+
}
462465
}
463-
if (!empty($signer['chain'][0]['subject']['UID'])) {
464-
$this->fileData->signers[$index]['uid'] = $signer['chain'][0]['subject']['UID'];
465-
} elseif (!empty($signer['chain'][0]['subject']['CN']) && preg_match('/^(?<key>.*):(?<value>.*), /', (string)$signer['chain'][0]['subject']['CN'], $matches)) {
466-
// Used by CFSSL
467-
$this->fileData->signers[$index]['uid'] = $matches['key'] . ':' . $matches['value'];
468-
} elseif (!empty($signer['chain'][0]['extensions']['subjectAltName'])) {
469-
// Used by old certs of LibreSign
470-
preg_match('/^(?<key>(email|account)):(?<value>.*)$/', (string)$signer['chain'][0]['extensions']['subjectAltName'], $matches);
471-
if ($matches) {
472-
if (str_ends_with($matches['value'], $this->host)) {
473-
$uid = str_replace('@' . $this->host, '', $matches['value']);
474-
$userFound = $this->userManager->get($uid);
466+
}
467+
}
468+
469+
private function resolveUid(array $chainArr): ?string {
470+
if (!empty($chainArr['subject']['UID'])) {
471+
return $chainArr['subject']['UID'];
472+
}
473+
if (!empty($chainArr['subject']['CN']) && preg_match('/^(?<key>.*):(?<value>.*), /', (string)$chainArr['subject']['CN'], $matches)) {
474+
return $matches['key'] . ':' . $matches['value'];
475+
}
476+
if (!empty($chainArr['extensions']['subjectAltName'])) {
477+
preg_match('/^(?<key>(email|account)):(?<value>.*)$/', (string)$chainArr['extensions']['subjectAltName'], $matches);
478+
if ($matches) {
479+
if (str_ends_with($matches['value'], $this->host)) {
480+
$uid = str_replace('@' . $this->host, '', $matches['value']);
481+
$userFound = $this->userManager->get($uid);
482+
if ($userFound) {
483+
return 'account:' . $uid;
484+
} else {
485+
$userFound = $this->userManager->getByEmail($matches['value']);
475486
if ($userFound) {
476-
$this->fileData->signers[$index]['uid'] = 'account:' . $uid;
487+
$userFound = current($userFound);
488+
return 'account:' . $userFound->getUID();
477489
} else {
478-
$userFound = $this->userManager->getByEmail($matches['value']);
479-
if ($userFound) {
480-
$userFound = current($userFound);
481-
$this->fileData->signers[$index]['uid'] = 'account:' . $userFound->getUID();
482-
} else {
483-
$this->fileData->signers[$index]['uid'] = 'email:' . $matches['value'];
484-
}
490+
return 'email:' . $matches['value'];
485491
}
492+
}
493+
} else {
494+
$userFound = $this->userManager->getByEmail($matches['value']);
495+
if ($userFound) {
496+
$userFound = current($userFound);
497+
return 'account:' . $userFound->getUID();
486498
} else {
487-
$userFound = $this->userManager->getByEmail($matches['value']);
499+
$userFound = $this->userManager->get($matches['value']);
488500
if ($userFound) {
489-
$userFound = current($userFound);
490-
$this->fileData->signers[$index]['uid'] = 'account:' . $userFound->getUID();
501+
return 'account:' . $userFound->getUID();
491502
} else {
492-
$userFound = $this->userManager->get($matches['value']);
493-
if ($userFound) {
494-
$this->fileData->signers[$index]['uid'] = 'account:' . $userFound->getUID();
495-
} else {
496-
$this->fileData->signers[$index]['uid'] = $matches['key'] . ':' . $matches['value'];
497-
}
503+
return $matches['key'] . ':' . $matches['value'];
498504
}
499505
}
500506
}
501507
}
502-
if (!empty($signer['chain'][0]['subject']['CN'])) {
503-
$this->fileData->signers[$index]['displayName'] = $signer['chain'][0]['subject']['CN'];
504-
} elseif (!empty($this->fileData->signers[$index]['uid'])) {
505-
$this->fileData->signers[$index]['displayName'] = $this->fileData->signers[$index]['uid'];
506-
}
507-
if (!empty($signer['timestamp'])) {
508-
$this->fileData->signers[$index]['timestamp'] = $signer['timestamp'];
509-
if ($signer['timestamp']['genTime'] instanceof \DateTimeInterface) {
510-
$this->fileData->signers[$index]['timestamp']['genTime'] = $signer['timestamp']['genTime']->format(DateTimeInterface::ATOM);
511-
}
512-
}
513-
for ($i = 1; $i < count($signer['chain']); $i++) {
514-
$this->fileData->signers[$index]['chain'][] = [
515-
'displayName' => $signer['chain'][$i]['name'],
516-
];
517-
}
518508
}
509+
return null;
519510
}
520511

521512
private function loadSigners(): void {

src/views/Validation.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,13 @@
255255
{{ dateFromSqlAnsi(signer.valid_to) }}
256256
</template>
257257
</NcListItem>
258-
<NcListItem v-if="signer.opened && signer.hash_algorithm"
258+
<NcListItem v-if="signer.opened && signer.signatureTypeSN"
259259
class="extra"
260260
compact
261261
:name="t('libresign', 'Hash algorithm:')">
262262
<template #name>
263263
<strong>{{ t('libresign', 'Hash algorithm:') }}</strong>
264-
{{ signer.hash_algorithm }}
264+
{{ signer.signatureTypeSN }}
265265
</template>
266266
</NcListItem>
267267
<NcListItem v-if="signer.opened && signer.timestamp && signer.timestamp.displayName"
@@ -379,11 +379,11 @@
379379
:name="t('libresign', 'Certificate chain:')">
380380
<template #name>
381381
<strong>{{ t('libresign', 'Certificate chain:') }}</strong>
382-
{{ signer.subject }}
382+
{{ signer.name }}
383383
</template>
384384
</NcListItem>
385385
<div v-if="signer.opened && signer.chain">
386-
<NcListItem v-for="(issuer, issuerIndex) in signer.chain"
386+
<NcListItem v-for="(issuer, issuerIndex) in signer.chain.filter(i => i.serialNumber !== signer.serialNumber)"
387387
:key="issuerIndex"
388388
class="extra-chain"
389389
compact

tests/integration/features/file/validate.feature

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ Feature: validate
3434
| key | value |
3535
| (jq).ocs.data.signers[0].me | false |
3636
| (jq).ocs.data.signers[0].identifyMethods | [{"method": "account","value": "signer1","mandatory": 1}] |
37-
| (jq).ocs.data.signers[0].subject | /C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/UID=account:signer1/CN=signer1-displayname |
37+
| (jq).ocs.data.signers[0].name | /C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/UID=account:signer1/CN=signer1-displayname |
38+
| (jq).ocs.data.signers[0].subject | {"C":"BR","ST":"State of Company","L":"City Name","O":"Organization","OU":"Organization Unit","UID":"account:signer1","CN":"signer1-displayname"} |
3839
| (jq).ocs.data.signers[0].signature_validation | {"id":1,"label":"Signature is valid."} |
39-
| (jq).ocs.data.signers[0].hash_algorithm | RSA-SHA256 |
40+
| (jq).ocs.data.signers[0].signatureTypeSN | RSA-SHA256 |
4041

4142
Scenario Outline: Unauthenticated user can fetch the validation ednpoint
4243
Given as user "admin"

tests/php/Unit/Service/FileServiceTest.php

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,15 @@ function (self $self, FileService $service): void {
284284
'name' => 'small_valid.pdf',
285285
'signers' => [
286286
[
287-
'displayName' => 'account:admin, admin',
288-
'subject' => '/C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/CN=account:admin, admin',
287+
'displayName' => '/C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/CN=account:admin, admin',
288+
'subject' => [
289+
'C' => 'BR',
290+
'ST' => 'State of Company',
291+
'L' => 'City Name',
292+
'O' => 'Organization',
293+
'OU' => 'Organization Unit',
294+
'CN' => 'account:admin, admin',
295+
],
289296
'valid_from' => '2025-10-20T13:26:00+00:00',
290297
'valid_to' => '2026-10-20T13:26:00+00:00',
291298
'signed' => '2025-10-20T13:31:43+00:00',
@@ -298,11 +305,131 @@ function (self $self, FileService $service): void {
298305
'id' => 3,
299306
'label' => 'Certificate issuer is unknown.',
300307
],
301-
'hash_algorithm' => 'RSA-SHA256',
302308
'field' => 'Signature1',
309+
'name' => '/C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/CN=account:admin, admin',
310+
'hash' => '4a5a1475',
311+
'issuer' => [
312+
'C' => 'BR',
313+
'ST' => 'State of Company',
314+
'L' => 'City Name',
315+
'O' => 'Organization',
316+
'OU' => 'Organization Unit',
317+
'CN' => 'Common Name',
318+
],
319+
'version' => 2,
320+
'serialNumber' => '0x4700D96F34F501CF0EA141E75F20643844393FFF',
321+
'serialNumberHex' => '4700D96F34F501CF0EA141E75F20643844393FFF',
322+
'validFrom' => '251020132600Z',
323+
'validTo' => '261020132600Z',
324+
'validFrom_time_t' => 1760966760,
325+
'validTo_time_t' => 1792502760,
326+
'signatureTypeSN' => 'RSA-SHA256',
327+
'signatureTypeLN' => 'sha256WithRSAEncryption',
328+
'signatureTypeNID' => 668,
329+
'purposes' => [
330+
1 => [true, false, 'sslclient'],
331+
2 => [false, false, 'sslserver'],
332+
3 => [false, false, 'nssslserver'],
333+
4 => [true, false, 'smimesign'],
334+
5 => [true, false, 'smimeencrypt'],
335+
6 => [false, false, 'crlsign'],
336+
7 => [true, true, 'any'],
337+
8 => [true, false, 'ocsphelper'],
338+
9 => [false, false, 'timestampsign'],
339+
],
340+
'extensions' => [
341+
'subjectAltName' => 'email:admin@email.tld',
342+
'basicConstraints' => 'CA:FALSE',
343+
'keyUsage' => 'Digital Signature, Non Repudiation, Key Encipherment',
344+
'extendedKeyUsage' => 'TLS Web Client Authentication, E-mail Protection',
345+
'subjectKeyIdentifier' => '76:21:81:44:79:1F:DC:85:E0:24:A1:1D:AA:8C:43:5B:0B:45:F9:48',
346+
'authorityKeyIdentifier' => '9D:6C:97:12:5D:29:8B:6D:C3:63:C0:0C:DF:28:99:18:81:17:61:69',
347+
],
348+
'isLibreSignRootCA' => false,
349+
'range' => [
350+
'offset1' => 0,
351+
'length1' => 1311,
352+
'offset2' => 31313,
353+
'length2' => 32829,
354+
],
355+
'signingTime' => [
356+
'date' => '2025-10-20 13:31:43.000000',
357+
'timezone_type' => 1,
358+
'timezone' => '+00:00',
359+
],
360+
'chain' => [
361+
0 => [
362+
'name' => '/C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/CN=account:admin, admin',
363+
'subject' => [
364+
'C' => 'BR',
365+
'ST' => 'State of Company',
366+
'L' => 'City Name',
367+
'O' => 'Organization',
368+
'OU' => 'Organization Unit',
369+
'CN' => 'account:admin, admin',
370+
],
371+
'hash' => '4a5a1475',
372+
'issuer' => [
373+
'C' => 'BR',
374+
'ST' => 'State of Company',
375+
'L' => 'City Name',
376+
'O' => 'Organization',
377+
'OU' => 'Organization Unit',
378+
'CN' => 'Common Name',
379+
],
380+
'version' => 2,
381+
'serialNumber' => '0x4700D96F34F501CF0EA141E75F20643844393FFF',
382+
'serialNumberHex' => '4700D96F34F501CF0EA141E75F20643844393FFF',
383+
'validFrom' => '251020132600Z',
384+
'validTo' => '261020132600Z',
385+
'validFrom_time_t' => 1760966760,
386+
'validTo_time_t' => 1792502760,
387+
'signatureTypeSN' => 'RSA-SHA256',
388+
'signatureTypeLN' => 'sha256WithRSAEncryption',
389+
'signatureTypeNID' => 668,
390+
'purposes' => [
391+
1 => [true, false, 'sslclient'],
392+
2 => [false, false, 'sslserver'],
393+
3 => [false, false, 'nssslserver'],
394+
4 => [true, false, 'smimesign'],
395+
5 => [true, false, 'smimeencrypt'],
396+
6 => [false, false, 'crlsign'],
397+
7 => [true, true, 'any'],
398+
8 => [true, false, 'ocsphelper'],
399+
9 => [false, false, 'timestampsign'],
400+
],
401+
'extensions' => [
402+
'keyUsage' => 'Digital Signature, Non Repudiation, Key Encipherment',
403+
'extendedKeyUsage' => 'TLS Web Client Authentication, E-mail Protection',
404+
'basicConstraints' => 'CA:FALSE',
405+
'subjectKeyIdentifier' => '76:21:81:44:79:1F:DC:85:E0:24:A1:1D:AA:8C:43:5B:0B:45:F9:48',
406+
'authorityKeyIdentifier' => '9D:6C:97:12:5D:29:8B:6D:C3:63:C0:0C:DF:28:99:18:81:17:61:69',
407+
'subjectAltName' => 'email:admin@email.tld',
408+
],
409+
'signature_validation' => [
410+
'id' => 1,
411+
'label' => 'Signature is valid.',
412+
],
413+
'isLibreSignRootCA' => false,
414+
'field' => 'Signature1',
415+
'range' => [
416+
'offset1' => 0,
417+
'length1' => 1311,
418+
'offset2' => 31313,
419+
'length2' => 32829,
420+
],
421+
'certificate_validation' => [
422+
'id' => 3,
423+
'label' => 'Certificate issuer is unknown.',
424+
],
425+
'valid_from' => '2025-10-20T13:26:00+00:00',
426+
'valid_to' => '2026-10-20T13:26:00+00:00',
427+
'displayName' => '/C=BR/ST=State of Company/L=City Name/O=Organization/OU=Organization Unit/CN=account:admin, admin',
428+
],
429+
],
303430
],
304431
],
305-
]
432+
],
306433
],
307434
];
308435
}

0 commit comments

Comments
 (0)