Skip to content

Commit a6b967d

Browse files
committed
Update VcSdJwt
1 parent ccb7212 commit a6b967d

4 files changed

Lines changed: 141 additions & 21 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims;
6+
7+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
8+
use SimpleSAML\OpenID\ValueAbstracts\ClaimInterface;
9+
10+
class VcCredentialStatusClaimBag implements ClaimInterface
11+
{
12+
/** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue[] */
13+
protected array $vcCredentialStatusClaimValueValues;
14+
15+
16+
public function __construct(
17+
VcCredentialStatusClaimValue $vcCredentialStatusClaimValue,
18+
VcCredentialStatusClaimValue ...$vcCredentialStatusClaimValueValues,
19+
) {
20+
$this->vcCredentialStatusClaimValueValues = [
21+
$vcCredentialStatusClaimValue,
22+
...$vcCredentialStatusClaimValueValues,
23+
];
24+
}
25+
26+
27+
/**
28+
* @return mixed[]
29+
*/
30+
public function jsonSerialize(): array
31+
{
32+
return array_map(
33+
fn(
34+
VcCredentialStatusClaimValue $vcCredentialStatusClaimValue,
35+
): array => $vcCredentialStatusClaimValue->jsonSerialize(),
36+
$this->getValue(),
37+
);
38+
}
39+
40+
41+
public function getName(): string
42+
{
43+
return ClaimsEnum::Credential_Subject->value;
44+
}
45+
46+
47+
/**
48+
* @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue[]
49+
*/
50+
public function getValue(): array
51+
{
52+
return $this->vcCredentialStatusClaimValueValues;
53+
}
54+
}

src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue;
1919
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimBag;
2020
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimValue;
21+
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimBag;
2122
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue;
2223
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag;
2324
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue;
@@ -214,6 +215,39 @@ public function buildVcCredentialStatusClaimValue(array $data): VcCredentialStat
214215
}
215216

216217

218+
/**
219+
* @param mixed[] $data
220+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
221+
* @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException
222+
*/
223+
public function buildVcCredentialStatusClaimBag(array $data): VcCredentialStatusClaimBag
224+
{
225+
// If this is a single credential status claim, wrap it in a
226+
// CredentialStatusClaimBag.
227+
if ($this->helpers->arr()->isAssociative($data)) {
228+
return new VcCredentialStatusClaimBag(
229+
$this->buildVcCredentialStatusClaimValue($data),
230+
);
231+
}
232+
233+
// We have multiple credential status claims. Wrap them in a
234+
// CredentialStatusClaimBag.
235+
$data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data);
236+
237+
$vcCredentialStatusClaims = [];
238+
foreach ($data as $credentialStatusClaimValueData) {
239+
$vcCredentialStatusClaims[] = $this->buildVcCredentialStatusClaimValue($credentialStatusClaimValueData);
240+
}
241+
242+
$firstCredentialStatusClaim = array_shift($vcCredentialStatusClaims);
243+
244+
return new VcCredentialStatusClaimBag(
245+
$firstCredentialStatusClaim,
246+
...$vcCredentialStatusClaims,
247+
);
248+
}
249+
250+
217251
/**
218252
* @param non-empty-array<mixed> $data
219253
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException

src/VerifiableCredentials/VcDataModel2/VcSdJwt.php

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\TypeClaimValue;
1515
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue;
1616
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimBag;
17-
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue;
17+
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimBag;
1818
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag;
1919
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimBag;
2020
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcIssuerClaimValue;
@@ -41,7 +41,7 @@ class VcSdJwt extends SdJwt implements VerifiableCredentialInterface
4141

4242
protected null|false|VcProofClaimValue $vcProofClaimValue = null;
4343

44-
protected null|false|VcCredentialStatusClaimValue $vcCredentialStatusClaimValue = null;
44+
protected null|false|VcCredentialStatusClaimBag $vcCredentialStatusClaimBag = null;
4545

4646
protected null|false|VcCredentialSchemaClaimBag $vcCredentialSchemaClaimBag = null;
4747

@@ -74,6 +74,10 @@ protected function validate(): void
7474
if (array_key_exists('vp', $payload)) {
7575
throw new VcDataModelException('SD-JWT VC MUST NOT contain a "vp" claim.');
7676
}
77+
78+
// Validate validFrom and validUntil claims
79+
$this->getValidFrom();
80+
$this->getValidUntil();
7781
}
7882

7983

@@ -244,15 +248,23 @@ public function getValidFrom(): DateTimeImmutable
244248

245249
try {
246250
$validFromStr = $this->helpers->type()->ensureNonEmptyString($validFrom, ClaimsEnum::ValidFrom->value);
247-
return $this->validFrom = $this->helpers->dateTime()->fromXsDateTime($validFromStr);
251+
$validFrom = $this->helpers->dateTime()->fromXsDateTime($validFromStr);
248252
} catch (Exception $exception) {
249253
throw new VcDataModelException('Invalid Valid From claim.', (int) $exception->getCode(), $exception);
250254
}
255+
256+
if ($validFrom->getTimestamp() - $this->timestampValidationLeeway->getInSeconds() > time()) {
257+
throw new VcDataModelException('Credential is not valid yet.');
258+
}
259+
260+
return $this->validFrom = $validFrom;
251261
}
252262

253263

254264
/**
255-
* Alias for getValidFrom to remain fully backwards compatible with consumers expecting getVcIssuanceDate
265+
* Alias for getValidFrom to remain fully backwards compatible with
266+
* consumers expecting `getVcIssuanceDate`.
267+
*
256268
* @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException
257269
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
258270
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
@@ -300,10 +312,16 @@ public function getValidUntil(): ?DateTimeImmutable
300312

301313
try {
302314
$validUntilStr = $this->helpers->type()->ensureNonEmptyString($validUntil, ClaimsEnum::ValidUntil->value);
303-
return $this->validUntil = $this->helpers->dateTime()->fromXsDateTime($validUntilStr);
315+
$validUntil = $this->helpers->dateTime()->fromXsDateTime($validUntilStr);
304316
} catch (Exception $exception) {
305317
throw new VcDataModelException('Invalid Valid Until claim.', (int) $exception->getCode(), $exception);
306318
}
319+
320+
if ($validUntil->getTimestamp() + $this->timestampValidationLeeway->getInSeconds() < time()) {
321+
throw new VcDataModelException('Credential is expired.');
322+
}
323+
324+
return $this->validUntil = $validUntil;
307325
}
308326

309327

@@ -349,30 +367,32 @@ public function getVcProof(): ?VcProofClaimValue
349367

350368
/**
351369
* @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException
370+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
371+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
352372
*/
353-
public function getVcCredentialStatus(): ?VcCredentialStatusClaimValue
373+
public function getVcCredentialStatus(): ?VcCredentialStatusClaimBag
354374
{
355-
if ($this->vcCredentialStatusClaimValue === false) {
375+
if ($this->vcCredentialStatusClaimBag === false) {
356376
return null;
357377
}
358378

359-
if ($this->vcCredentialStatusClaimValue instanceof VcCredentialStatusClaimValue) {
360-
return $this->vcCredentialStatusClaimValue;
379+
if ($this->vcCredentialStatusClaimBag instanceof VcCredentialStatusClaimBag) {
380+
return $this->vcCredentialStatusClaimBag;
361381
}
362382

363383
$vcCredentialStatus = $this->getPayloadClaim(ClaimsEnum::Credential_Status->value);
364384

365385
if (is_null($vcCredentialStatus)) {
366-
$this->vcCredentialStatusClaimValue = false;
386+
$this->vcCredentialStatusClaimBag = false;
367387
return null;
368388
}
369389

370390
if (!is_array($vcCredentialStatus)) {
371391
throw new VcDataModelException('Invalid Credential Status claim.');
372392
}
373393

374-
return $this->vcCredentialStatusClaimValue = $this->claimFactory->forVcDataModel2()
375-
->buildVcCredentialStatusClaimValue($vcCredentialStatus);
394+
return $this->vcCredentialStatusClaimBag = $this->claimFactory->forVcDataModel2()
395+
->buildVcCredentialStatusClaimBag($vcCredentialStatus);
376396
}
377397

378398

tests/src/VerifiableCredentials/VcDataModel2/VcSdJwtTest.php

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DateTimeImmutable;
88
use Jose\Component\Signature\JWS;
99
use Jose\Component\Signature\Signature;
10+
use PHPUnit\Framework\Attributes\UsesClass;
1011
use PHPUnit\Framework\MockObject\MockObject;
1112
use PHPUnit\Framework\TestCase;
1213
use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum;
@@ -22,6 +23,7 @@
2223
use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel2\VcSdJwt;
2324

2425
#[\PHPUnit\Framework\Attributes\CoversClass(VcSdJwt::class)]
26+
#[UsesClass(\SimpleSAML\OpenID\Helpers\DateTime::class)]
2527
final class VcSdJwtTest extends TestCase
2628
{
2729
/** @var \Jose\Component\Signature\Signature&\PHPUnit\Framework\MockObject\MockObject */
@@ -36,13 +38,10 @@ final class VcSdJwtTest extends TestCase
3638
/** @var \SimpleSAML\OpenID\Helpers\Json&\PHPUnit\Framework\MockObject\MockObject */
3739
protected MockObject $jsonHelperMock;
3840

39-
/** @var \SimpleSAML\OpenID\Helpers\DateTime&\PHPUnit\Framework\MockObject\MockObject */
40-
protected MockObject $dateTimeHelperMock;
41-
4241
/** @var \SimpleSAML\OpenID\Factories\ClaimFactory&\PHPUnit\Framework\MockObject\Stub */
4342
protected \PHPUnit\Framework\MockObject\Stub $claimFactoryMock;
4443

45-
protected array $validPayload = [
44+
protected array $expiredPayload = [
4645
"@context" => [
4746
"https://www.w3.org/ns/credentials/v2",
4847
"https://www.w3.org/2018/credentials/examples/v1",
@@ -69,6 +68,8 @@ final class VcSdJwtTest extends TestCase
6968
"sub" => "did:example:123",
7069
];
7170

71+
protected array $validPayload;
72+
7273
protected array $sampleHeader = [
7374
'alg' => 'ES256',
7475
'typ' => 'vc+sd-jwt',
@@ -94,8 +95,16 @@ protected function setUp(): void
9495
$this->helpersMock->method('type')->willReturn($typeHelperMock);
9596
$arrHelperMock = $this->createMock(Helpers\Arr::class);
9697
$this->helpersMock->method('arr')->willReturn($arrHelperMock);
97-
$this->dateTimeHelperMock = $this->createMock(Helpers\DateTime::class);
98-
$this->helpersMock->method('dateTime')->willReturn($this->dateTimeHelperMock);
98+
$dateTimeHelperMock = $this->createMock(Helpers\DateTime::class);
99+
$this->helpersMock->method('dateTime')->willReturn($dateTimeHelperMock);
100+
101+
$realDateTimeHelper = new Helpers\DateTime();
102+
$dateTimeHelperMock->method('fromXsDateTime')
103+
->willReturnCallback(fn(string $input): \DateTimeImmutable => $realDateTimeHelper->fromXsDateTime($input));
104+
$dateTimeHelperMock->method('fromTimestamp')
105+
->willReturnCallback(
106+
fn(int $timestamp): \DateTimeImmutable => $realDateTimeHelper->fromTimestamp($timestamp),
107+
);
99108

100109
$typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0);
101110
$typeHelperMock->method('ensureInt')->willReturnArgument(0);
@@ -110,6 +119,8 @@ protected function setUp(): void
110119

111120
$this->claimFactoryMock = $this->createStub(ClaimFactory::class);
112121

122+
$this->validPayload = $this->expiredPayload;
123+
113124
$this->validPayload['exp'] = time() + 3600;
114125
$this->validPayload['validUntil'] = (new \DateTimeImmutable())
115126
->modify('+1 hour')
@@ -119,12 +130,15 @@ protected function setUp(): void
119130

120131
protected function sut(): VcSdJwt
121132
{
133+
$leewayMock = $this->createMock(\SimpleSAML\OpenID\Decorators\DateIntervalDecorator::class);
134+
$leewayMock->method('getInSeconds')->willReturn(0);
135+
122136
return new VcSdJwt(
123137
$this->jwsDecoratorMock,
124138
$this->createStub(\SimpleSAML\OpenID\Jws\JwsVerifierDecorator::class),
125139
$this->createStub(\SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory::class),
126140
$this->createStub(\SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator::class),
127-
$this->createStub(\SimpleSAML\OpenID\Decorators\DateIntervalDecorator::class),
141+
$leewayMock,
128142
$this->helpersMock,
129143
$this->claimFactoryMock,
130144
);
@@ -159,8 +173,6 @@ public function testGetPropertiesReturnTypes(): void
159173

160174
$this->claimFactoryMock->method('forVcDataModel2')->willReturn($vcDataModelClaimFactoryMock);
161175

162-
$this->dateTimeHelperMock->method('fromXsDateTime')->willReturn(new DateTimeImmutable());
163-
164176
$this->assertInstanceOf(VcAtContextClaimValue::class, $sut->getVcAtContext());
165177
$this->assertIsString($sut->getVcId());
166178
$this->assertInstanceOf(TypeClaimValue::class, $sut->getVcType());

0 commit comments

Comments
 (0)