1212use SimpleSAML \Module \oidc \Repositories \UserRepository ;
1313use SimpleSAML \Module \oidc \Server \Exceptions \OidcServerException ;
1414use SimpleSAML \Module \oidc \Services \LoggerService ;
15- use SimpleSAML \Module \oidc \Utils \DidKeyResolver ;
1615use SimpleSAML \Module \oidc \Utils \RequestParamsResolver ;
1716use SimpleSAML \Module \oidc \Utils \Routes ;
1817use SimpleSAML \OpenID \Algorithms \SignatureAlgorithmEnum ;
1918use SimpleSAML \OpenID \Codebooks \AtContextsEnum ;
2019use SimpleSAML \OpenID \Codebooks \ClaimsEnum ;
2120use SimpleSAML \OpenID \Codebooks \CredentialTypesEnum ;
2221use SimpleSAML \OpenID \Codebooks \HttpMethodsEnum ;
22+ use SimpleSAML \OpenID \Did ;
23+ use SimpleSAML \OpenID \Exceptions \OpenId4VciProofException ;
2324use SimpleSAML \OpenID \Jwk ;
2425use SimpleSAML \OpenID \VerifiableCredentials ;
2526use 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 [
0 commit comments