Skip to content

Commit a92df35

Browse files
authored
Merge pull request #5722 from LibreSign/feat/return-more-data-from-validation-endpoint
feat: return more data from validation endpoint
2 parents b3c19e3 + aff97bd commit a92df35

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

0 commit comments

Comments
 (0)