Skip to content

Commit a049d09

Browse files
committed
WIP
1 parent a89915d commit a049d09

File tree

6 files changed

+99
-334
lines changed

6 files changed

+99
-334
lines changed

routing/services/services.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ services:
133133
SimpleSAML\OpenID\Jwks:
134134
factory: [ '@SimpleSAML\Module\oidc\Factories\JwksFactory', 'build' ]
135135
SimpleSAML\OpenID\Jwk: ~
136+
SimpleSAML\OpenID\Did: ~
137+
136138

137139
# SSP
138140
SimpleSAML\Database:

src/Controllers/Admin/VerifiableCredentailsTestController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public function verifiableCredentialIssuance(Request $request): Response
129129
is_null($selectedCredentialConfigurationId) ||
130130
!in_array($selectedCredentialConfigurationId, $credentialConfigurationIdsSupported, true)
131131
) {
132-
$selectedCredentialConfigurationId = current($credentialConfigurationIdsSupported);
132+
$selectedCredentialConfigurationId = current($credentialConfigurationIdsSupported);
133133
}
134134

135135
$credentialOfferQrUri = null;
@@ -267,7 +267,7 @@ public function verifiableCredentialIssuance(Request $request): Response
267267
'authSourceActionRoute',
268268
'authSource',
269269
'credentialConfigurationIdsSupported',
270-
'selectedCredentialConfigurationId'
270+
'selectedCredentialConfigurationId',
271271
),
272272
RoutesEnum::AdminTestVerifiableCredentialIssuance->value,
273273
);

src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php

Lines changed: 91 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
use SimpleSAML\Module\oidc\Repositories\UserRepository;
1313
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1414
use SimpleSAML\Module\oidc\Services\LoggerService;
15-
use SimpleSAML\Module\oidc\Utils\DidKeyResolver;
1615
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
1716
use SimpleSAML\Module\oidc\Utils\Routes;
1817
use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum;
1918
use SimpleSAML\OpenID\Codebooks\AtContextsEnum;
2019
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2120
use SimpleSAML\OpenID\Codebooks\CredentialTypesEnum;
2221
use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum;
22+
use SimpleSAML\OpenID\Did;
23+
use SimpleSAML\OpenID\Exceptions\OpenId4VciProofException;
2324
use SimpleSAML\OpenID\Jwk;
2425
use SimpleSAML\OpenID\VerifiableCredentials;
2526
use Symfony\Component\HttpFoundation\Request;
@@ -41,7 +42,7 @@ public function __construct(
4142
protected readonly LoggerService $loggerService,
4243
protected readonly RequestParamsResolver $requestParamsResolver,
4344
protected readonly UserRepository $userRepository,
44-
protected readonly DidKeyResolver $didKeyResolver,
45+
protected readonly Did $did,
4546
) {
4647
if (!$this->moduleConfig->getVerifiableCredentialEnabled()) {
4748
throw OidcServerException::forbidden('Verifiable Credential capabilities not enabled');
@@ -77,82 +78,24 @@ public function credential(Request $request): Response
7778
);
7879
}
7980

80-
// Validate credential request, including proof
81-
if (isset($requestData['proof']) && isset($requestData['proof']['proof_type']) &&
82-
$requestData['proof']['proof_type'] === 'jwt' && isset($requestData['proof']['jwt'])) {
83-
84-
$proofJwt = $requestData['proof']['jwt'];
85-
$this->loggerService->debug('Verifying proof JWT: ' . $proofJwt);
86-
87-
try {
88-
// Parse the JWT to extract header and payload
89-
$jwtParts = explode('.', $proofJwt);
90-
if (count($jwtParts) !== 3) {
91-
throw OidcServerException::invalidRequest('Invalid JWT format in proof');
92-
}
93-
94-
$header = json_decode(Base64Url::decode($jwtParts[0]), true);
95-
$payload = json_decode(Base64Url::decode($jwtParts[1]), true);
96-
97-
if (!isset($payload['iss'])) {
98-
throw OidcServerException::invalidRequest('Missing issuer (iss) in proof JWT');
99-
}
100-
101-
$issuer = $payload['iss'];
102-
$this->loggerService->debug('Proof JWT issuer: ' . $issuer);
103-
104-
// Check if the issuer is a did:key
105-
if (str_starts_with($issuer, 'did:key:')) {
106-
$this->loggerService->debug('Extracting JWK from did:key: ' . $issuer);
107-
108-
// Extract JWK from did:key
109-
$jwk = $this->didKeyResolver->extractJwkFromDidKey($issuer);
110-
111-
// If kid is present in the header, add it to the JWK
112-
if (isset($header['kid'])) {
113-
$jwk['kid'] = $header['kid'];
114-
} else {
115-
// If no kid in header, use the did:key as kid
116-
$jwk['kid'] = $issuer;
117-
}
118-
119-
$this->loggerService->debug('Extracted JWK: ', $jwk);
120-
121-
// TODO: Verify the JWT signature using the extracted JWK
122-
// This would typically involve using a JWT library to verify the signature
123-
// For now, we'll just log that we've extracted the JWK successfully
124-
$this->loggerService->debug('JWK extracted successfully from did:key');
125-
}
126-
} catch (\Exception $e) {
127-
$this->loggerService->error('Error processing proof JWT: ' . $e->getMessage());
128-
throw OidcServerException::invalidRequest('Error processing proof JWT: ' . $e->getMessage());
129-
}
130-
}
131-
132-
/**
133-
* Sample proof structure:
134-
* 'proof' =>
135-
* array (
136-
* 'proof_type' => 'jwt',
137-
* 'jwt' => 'eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2Iiwia2lkIjoiZGlkOmtleTp6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JyU2ZYMkJVeHNVaW5QbVA3QUVzZEN4OWpQYlV0ZkIzWXN2MTd4TGpyZkMxeDNVZmlMTWtyeWdTZDJMeWltQ3RGejhHWlBqOFFrMUJFU0F6M21LWGRCTEpuUHNNQ0R4Nm9QNjNuZVpmR1NKelF5SjRLVlN6Nmt4UTJQOTE4NGdXS1FnI3oyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnJTZlgyQlV4c1VpblBtUDdBRXNkQ3g5alBiVXRmQjNZc3YxN3hManJmQzF4M1VmaUxNa3J5Z1NkMkx5aW1DdEZ6OEdaUGo4UWsxQkVTQXozbUtYZEJMSm5Qc01DRHg2b1A2M25lWmZHU0p6UXlKNEtWU3o2a3hRMlA5MTg0Z1dLUWcifQ.eyJhdWQiOiJodHRwczovL2lkcC5taXZhbmNpLmluY3ViYXRvci5oZXhhYS5ldSIsImlhdCI6MTc0ODUxNDE0NywiZXhwIjoxNzQ4NTE0ODA3LCJpc3MiOiJkaWQ6a2V5OnoyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnJTZlgyQlV4c1VpblBtUDdBRXNkQ3g5alBiVXRmQjNZc3YxN3hManJmQzF4M1VmaUxNa3J5Z1NkMkx5aW1DdEZ6OEdaUGo4UWsxQkVTQXozbUtYZEJMSm5Qc01DRHg2b1A2M25lWmZHU0p6UXlKNEtWU3o2a3hRMlA5MTg0Z1dLUWciLCJqdGkiOiJiMmNlZDQ2Yi0zOWNiLTRkZDAtYmQxZS1hNzY5ZWNlOWUxMTIifQ.SPdMSnrfF8ybhfYluzz5OrfWJQDOpCu7-of8zVbp5UR89GaB7j14Egext1h9pYgl6JwIP8zibUjTSc8JLVYuvA',
138-
* ),
139-
*/
81+
// TODO mivanci Validate credential request
14082

14183
// TODO mivanci Check / handle credential_identifier parameter.
14284

14385
$credentialConfigurationId = $requestData[ClaimsEnum::CredentialConfigurationId->value] ?? null;
14486

14587
if (is_null($credentialConfigurationId)) {
14688
// Check per draft 14
147-
if (is_array(
148-
$credentialDefinitionType =
89+
if (
90+
is_array(
91+
$credentialDefinitionType =
14992
$requestData[ClaimsEnum::CredentialDefinition->value][ClaimsEnum::Type->value],
150-
)
93+
)
15194
) {
15295
$credentialConfigurationId =
153-
$this->moduleConfig->getCredentialConfigurationIdForCredentialDefinitionType(
154-
$credentialDefinitionType,
155-
);
96+
$this->moduleConfig->getCredentialConfigurationIdForCredentialDefinitionType(
97+
$credentialDefinitionType,
98+
);
15699
}
157100
}
158101

@@ -206,6 +149,85 @@ public function credential(Request $request): Response
206149
}
207150
}
208151

152+
// Placeholder sub identifier. Will do if proof is not provided.
153+
$sub = $this->moduleConfig->getIssuer() . '/sub/' . $userId;
154+
155+
// Validate proof, if provided.
156+
// TODO mivanci consider making proof mandatory (in issuer metadata).
157+
if (
158+
isset($requestData['proof']['proof_type']) &&
159+
isset($requestData['proof']['jwt']) &&
160+
$requestData['proof']['proof_type'] === 'jwt'
161+
) {
162+
$proofJwt = $requestData['proof']['jwt'];
163+
$this->loggerService->debug('Verifying proof JWT: ' . $proofJwt);
164+
165+
try {
166+
/**
167+
* Sample proof structure:
168+
* 'proof' =>
169+
* array (
170+
* 'proof_type' => 'jwt',
171+
* 'jwt' => 'eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2Iiwia2lkIjoiZGlkOmtleTp6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JyU2ZYMkJVeHNVaW5QbVA3QUVzZEN4OWpQYlV0ZkIzWXN2MTd4TGpyZkMxeDNVZmlMTWtyeWdTZDJMeWltQ3RGejhHWlBqOFFrMUJFU0F6M21LWGRCTEpuUHNNQ0R4Nm9QNjNuZVpmR1NKelF5SjRLVlN6Nmt4UTJQOTE4NGdXS1FnI3oyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnJTZlgyQlV4c1VpblBtUDdBRXNkQ3g5alBiVXRmQjNZc3YxN3hManJmQzF4M1VmaUxNa3J5Z1NkMkx5aW1DdEZ6OEdaUGo4UWsxQkVTQXozbUtYZEJMSm5Qc01DRHg2b1A2M25lWmZHU0p6UXlKNEtWU3o2a3hRMlA5MTg0Z1dLUWcifQ.eyJhdWQiOiJodHRwczovL2lkcC5taXZhbmNpLmluY3ViYXRvci5oZXhhYS5ldSIsImlhdCI6MTc0ODUxNDE0NywiZXhwIjoxNzQ4NTE0ODA3LCJpc3MiOiJkaWQ6a2V5OnoyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnJTZlgyQlV4c1VpblBtUDdBRXNkQ3g5alBiVXRmQjNZc3YxN3hManJmQzF4M1VmaUxNa3J5Z1NkMkx5aW1DdEZ6OEdaUGo4UWsxQkVTQXozbUtYZEJMSm5Qc01DRHg2b1A2M25lWmZHU0p6UXlKNEtWU3o2a3hRMlA5MTg0Z1dLUWciLCJqdGkiOiJiMmNlZDQ2Yi0zOWNiLTRkZDAtYmQxZS1hNzY5ZWNlOWUxMTIifQ.SPdMSnrfF8ybhfYluzz5OrfWJQDOpCu7-of8zVbp5UR89GaB7j14Egext1h9pYgl6JwIP8zibUjTSc8JLVYuvA',
172+
* ),
173+
*
174+
* Sphereon proof in credential request
175+
* {
176+
* "typ": "openid4vci-proof+jwt",
177+
* "alg": "ES256",
178+
* "kid": "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbrSfX2BUxsUinPmP7AEsdCx9jPbUtfB3Ysv17xLjrfC1x3UfiLMkrygSd2LyimCtFz8GZPj8Qk1BESAz3mKXdBLJnPsMCDx6oP63neZfGSJzQyJ4KVSz6kxQ2P9184gWKQg#z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbrSfX2BUxsUinPmP7AEsdCx9jPbUtfB3Ysv17xLjrfC1x3UfiLMkrygSd2LyimCtFz8GZPj8Qk1BESAz3mKXdBLJnPsMCDx6oP63neZfGSJzQyJ4KVSz6kxQ2P9184gWKQg"
179+
* }
180+
* {
181+
* "aud": "https://idp.mivanci.incubator.hexaa.eu",
182+
* "iat": 1748514147,
183+
* "exp": 1748514807,
184+
* "iss": "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbrSfX2BUxsUinPmP7AEsdCx9jPbUtfB3Ysv17xLjrfC1x3UfiLMkrygSd2LyimCtFz8GZPj8Qk1BESAz3mKXdBLJnPsMCDx6oP63neZfGSJzQyJ4KVSz6kxQ2P9184gWKQg",
185+
* "jti": "b2ced46b-39cb-4dd0-bd1e-a769ece9e112"
186+
* }
187+
*/
188+
$proof = $this->verifiableCredentials->openId4VciProofFactory()->fromToken($proofJwt);
189+
(in_array($this->moduleConfig->getIssuer(), $proof->getAudience())) ||
190+
throw new OpenId4VciProofException('Invalid Proof audience.');
191+
192+
$kid = $proof->getKeyId();
193+
if (is_string($kid) && str_starts_with($kid, 'did:key:z')) {
194+
// The fragment (#z2dmzD...) typically points to a specific verification method within the DID's
195+
// context. For did:key, since the DID is the key, this fragment often just refers to the key
196+
// itself.
197+
($didKey = strtok($kid, '#')) || throw new OpenId4VciProofException(
198+
'Error getting did:key without fragment. Value was: ' . $kid,
199+
);
200+
201+
$jwk = $this->did->didKeyResolver()->extractJwkFromDidKey($didKey);
202+
203+
$proof->verifyWithKey($jwk);
204+
205+
$this->loggerService->debug('Proof verified successfully using did:key ' . $didKey);
206+
// Set it as a subject identifier (bind it).
207+
$sub = $didKey;
208+
} else {
209+
$this->loggerService->warning(
210+
'Proof currently not supported. ',
211+
['header' => $proof->getHeader(), 'payload' => $proof->getPayload()],
212+
);
213+
}
214+
} catch (\Exception $e) {
215+
$message = 'Error processing proof JWT: ' . $e->getMessage();
216+
$this->loggerService->error($message);
217+
return $this->routes->newJsonErrorResponse(
218+
'invalid_proof',
219+
$message,
220+
);
221+
}
222+
}
223+
224+
// Also make sure that the subject identifier is in credentialSubject claim.
225+
$this->setCredentialClaimValue(
226+
$credentialSubject,
227+
[ClaimsEnum::Credential_Subject->value, ClaimsEnum::Id->value],
228+
$sub,
229+
);
230+
209231
$signingKey = $this->jwk->jwkDecoratorFactory()->fromPkcs1Or8KeyFile(
210232
$this->moduleConfig->getProtocolPrivateKeyPath(),
211233
null,
@@ -224,7 +246,6 @@ public function credential(Request $request): Response
224246

225247
$issuerDid = 'did:jwk:' . $base64PublicKey;
226248

227-
228249
$issuedAt = new \DateTimeImmutable();
229250

230251
$vcId = $this->moduleConfig->getIssuer() . '/vc/' . uniqid();
@@ -254,7 +275,7 @@ public function credential(Request $request): Response
254275
//ClaimsEnum::Iss->value => 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/jwks',
255276
ClaimsEnum::Iat->value => $issuedAt->getTimestamp(),
256277
ClaimsEnum::Nbf->value => $issuedAt->getTimestamp(),
257-
ClaimsEnum::Sub->value => $this->moduleConfig->getIssuer() . '/sub/' . $userId,
278+
ClaimsEnum::Sub->value => $sub,
258279
ClaimsEnum::Jti->value => $vcId,
259280
],
260281
[

src/ModuleConfig.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -801,9 +801,11 @@ public function getCredentialConfigurationIdsSupported(): array
801801

802802
public function getCredentialConfigurationIdForCredentialDefinitionType(array $credentialDefinitionType): ?string
803803
{
804-
foreach ($this->getCredentialConfigurationsSupported() as $credentialConfigurationId => $credentialConfiguration) {
804+
foreach (
805+
$this->getCredentialConfigurationsSupported() as $credentialConfigurationId => $credentialConfiguration
806+
) {
805807
$configuredType =
806-
$credentialConfiguration[ClaimsEnum::CredentialDefinition->value][ClaimsEnum::Type->value];
808+
$credentialConfiguration[ClaimsEnum::CredentialDefinition->value][ClaimsEnum::Type->value];
807809

808810
if ($configuredType === $credentialDefinitionType) {
809811
return $credentialConfigurationId;

src/Services/Container.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@
103103
use SimpleSAML\Module\oidc\Stores\Session\LogoutTicketStoreDb;
104104
use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor;
105105
use SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder;
106-
use SimpleSAML\Module\oidc\Utils\DidKeyResolver;
107106
use SimpleSAML\Module\oidc\Utils\FederationCache;
108107
use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator;
109108
use SimpleSAML\Module\oidc\Utils\JwksResolver;
@@ -231,9 +230,6 @@ public function __construct()
231230
$requestParamsResolver = new RequestParamsResolver($helpers, $core, $federation);
232231
$this->services[RequestParamsResolver::class] = $requestParamsResolver;
233232

234-
$didKeyResolver = new DidKeyResolver();
235-
$this->services[DidKeyResolver::class] = $didKeyResolver;
236-
237233
$clientEntityFactory = new ClientEntityFactory(
238234
$sspBridge,
239235
$helpers,

0 commit comments

Comments
 (0)