-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Issue 30525 #30692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Issue 30525 #30692
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
a6d93e9
Enhance SupportedCredentialConfiguration to support optional claims o…
francis-pouatcha cba3f18
Merge branch 'keycloak:main' into issue-30419
francis-pouatcha 8db7d15
Modifications from issue comment https://github.com/keycloak/keycloak…
francis-pouatcha e06264d
Fix on issue 30525
francis-pouatcha 5f5f2d1
Replaced code param with pre-authorized_code as stated by spec
francis-pouatcha aa63e2c
Removed mandatory presence of nbf claim as it is optional in the spec
francis-pouatcha 803ac11
Serializing a proof type enum
francis-pouatcha 2b6f429
Fix: verifier wallet provided proof
francis-pouatcha e4a1ac2
Merge branch 'keycloak:main' into issue-30525
francis-pouatcha 7152b53
Merged from main
francis-pouatcha b5e52b3
Fix: removed dupplicate instantiation of service
francis-pouatcha cc415b1
Fix(test): set code request param grant ty urn:ietf:params:oauth:gran…
francis-pouatcha af6abfd
Fix(test): set code request param grant ty urn:ietf:params:oauth:gran…
francis-pouatcha 80dd9c7
Fix(test): remove check of nbf from tests, as nbf claim not mandatory…
francis-pouatcha b5e326d
Processing feedbacks from first review comments
francis-pouatcha 39100ed
Processing feedbacks from first review comments
francis-pouatcha 7ea1d46
Merging from main
francis-pouatcha 346b415
Processing review comments
francis-pouatcha c7d69ab
Deleting from last main merge
francis-pouatcha 3753e0f
Processing more review comments
francis-pouatcha 15c381d
Merged from upstream
francis-pouatcha 98ba316
Merged conficts from main
francis-pouatcha d8cd042
Removed unnecessary toString. Proofreading from review comments.
francis-pouatcha 5f99e16
Processed review comments, formating, coding rules
francis-pouatcha c93bdc3
Merge branch 'keycloak:main' into issue-30525
francis-pouatcha 9717c87
Review comments. Final fields on immutable classes.
francis-pouatcha File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
193 changes: 134 additions & 59 deletions
193
services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java
Large diffs are not rendered by default.
Oops, something went wrong.
74 changes: 74 additions & 0 deletions
74
services/src/main/java/org/keycloak/protocol/oid4vc/issuance/VCIssuanceContext.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * Copyright 2024 Red Hat, Inc. and/or its affiliates | ||
| * and other contributors as indicated by the @author tags. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.keycloak.protocol.oid4vc.issuance; | ||
|
|
||
| import org.keycloak.protocol.oid4vc.model.CredentialRequest; | ||
| import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration; | ||
| import org.keycloak.protocol.oid4vc.model.VerifiableCredential; | ||
| import org.keycloak.services.managers.AuthenticationManager; | ||
|
|
||
| /** | ||
| * Holds the verifiable credential to sign and additional context information. | ||
| * | ||
| * Helps keeps the {@link VerifiableCredential} as clean pojo. Without any risk to | ||
| * mistakenly serialize unwanted information. | ||
| * | ||
| * @author <a href="mailto:[email protected]">Francis Pouatcha</a> | ||
| */ | ||
| public class VCIssuanceContext { | ||
| private VerifiableCredential verifiableCredential; | ||
|
|
||
| private SupportedCredentialConfiguration credentialConfig; | ||
| private CredentialRequest credentialRequest; | ||
| private AuthenticationManager.AuthResult authResult; | ||
|
|
||
| public VerifiableCredential getVerifiableCredential() { | ||
| return verifiableCredential; | ||
| } | ||
|
|
||
| public VCIssuanceContext setVerifiableCredential(VerifiableCredential verifiableCredential) { | ||
| this.verifiableCredential = verifiableCredential; | ||
| return this; | ||
| } | ||
|
|
||
| public SupportedCredentialConfiguration getCredentialConfig() { | ||
| return credentialConfig; | ||
| } | ||
|
|
||
| public VCIssuanceContext setCredentialConfig(SupportedCredentialConfiguration credentialConfig) { | ||
| this.credentialConfig = credentialConfig; | ||
| return this; | ||
| } | ||
|
|
||
| public CredentialRequest getCredentialRequest() { | ||
| return credentialRequest; | ||
| } | ||
|
|
||
| public VCIssuanceContext setCredentialRequest(CredentialRequest credentialRequest) { | ||
| this.credentialRequest = credentialRequest; | ||
| return this; | ||
| } | ||
|
|
||
| public AuthenticationManager.AuthResult getAuthResult() { | ||
| return authResult; | ||
| } | ||
|
|
||
| public VCIssuanceContext setAuthResult(AuthenticationManager.AuthResult authResult) { | ||
| this.authResult = authResult; | ||
| return this; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
207 changes: 207 additions & 0 deletions
207
.../main/java/org/keycloak/protocol/oid4vc/issuance/signing/JwtProofBasedSigningService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| /* | ||
| * Copyright 2024 Red Hat, Inc. and/or its affiliates | ||
| * and other contributors as indicated by the @author tags. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.keycloak.protocol.oid4vc.issuance.signing; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Arrays; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
|
|
||
| import org.jboss.logging.Logger; | ||
| import org.keycloak.common.VerificationException; | ||
| import org.keycloak.crypto.SignatureVerifierContext; | ||
| import org.keycloak.jose.jwk.JWK; | ||
| import org.keycloak.jose.jws.JWSHeader; | ||
| import org.keycloak.jose.jws.JWSInput; | ||
| import org.keycloak.jose.jws.JWSInputException; | ||
| import org.keycloak.models.KeycloakSession; | ||
| import org.keycloak.protocol.oid4vc.issuance.VCIssuanceContext; | ||
| import org.keycloak.protocol.oid4vc.issuance.VCIssuerException; | ||
| import org.keycloak.protocol.oid4vc.model.Proof; | ||
| import org.keycloak.protocol.oid4vc.model.ProofType; | ||
| import org.keycloak.protocol.oid4vc.model.ProofTypeJWT; | ||
| import org.keycloak.protocol.oid4vc.model.ProofTypesSupported; | ||
| import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration; | ||
| import org.keycloak.representations.AccessToken; | ||
| import org.keycloak.util.JsonSerialization; | ||
|
|
||
| /** | ||
| * Common signing service logic to handle proofs. | ||
| * | ||
| * @author <a href="mailto:[email protected]">Francis Pouatcha</a> | ||
| */ | ||
| public abstract class JwtProofBasedSigningService<T> extends SigningService<T> { | ||
|
|
||
| private static final Logger LOGGER = Logger.getLogger(JwtProofBasedSigningService.class); | ||
| private static final String CRYPTOGRAPHIC_BINDING_METHOD_JWK = "jwk"; | ||
| public static final String PROOF_JWT_TYP="openid4vci-proof+jwt"; | ||
|
|
||
| protected JwtProofBasedSigningService(KeycloakSession keycloakSession, String keyId, String format, String type) { | ||
| super(keycloakSession, keyId, format, type); | ||
| } | ||
|
|
||
| /* | ||
| * Validates a proof provided by the client if any. | ||
| * | ||
| * Returns null if there is no need to include a key binding in the credential | ||
| * | ||
| * Return the JWK to be included as key binding in the JWK if the provided proof was correctly validated | ||
| * | ||
| * @param vcIssuanceContext | ||
| * @return | ||
| * @throws VCIssuerException | ||
| * @throws JWSInputException | ||
| * @throws VerificationException | ||
| * @throws IllegalStateException: is credential type badly configured | ||
| * @throws IOException | ||
| */ | ||
| protected JWK validateProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException, JWSInputException, VerificationException, IOException { | ||
|
|
||
| Optional<Proof> optionalProof = getProofFromContext(vcIssuanceContext); | ||
|
|
||
| if (optionalProof.isEmpty()) { | ||
| return null; // No proof support | ||
| } | ||
|
|
||
| // Check key binding config for jwt. Only type supported. | ||
| checkCryptographicKeyBinding(vcIssuanceContext); | ||
|
|
||
| JWSInput jwsInput = getJwsInput(optionalProof.get()); | ||
| JWSHeader jwsHeader = jwsInput.getHeader(); | ||
| validateJwsHeader(vcIssuanceContext, jwsHeader); | ||
|
|
||
| JWK jwk = Optional.ofNullable(jwsHeader.getKey()) | ||
| .orElseThrow(() -> new VCIssuerException("Missing binding key. Make sure provided JWT contains the jwk jwsHeader claim.")); | ||
|
|
||
| // Parsing the Proof as an access token shall work, as a proof is a strict subset of an access token. | ||
| AccessToken proofPayload = JsonSerialization.readValue(jwsInput.getContent(), AccessToken.class); | ||
| validateProofPayload(vcIssuanceContext, proofPayload); | ||
|
|
||
| SignatureVerifierContext signatureVerifierContext = getVerifier(jwk, jwsHeader.getAlgorithm().name()); | ||
| if (signatureVerifierContext == null) { | ||
| throw new VCIssuerException("No verifier configured for " + jwsHeader.getAlgorithm()); | ||
| } | ||
| if (!signatureVerifierContext.verify(jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jwsInput.getSignature())) { | ||
| throw new VCIssuerException("Could not verify provided proof"); | ||
| } | ||
|
|
||
| return jwk; | ||
| } | ||
|
|
||
| private void checkCryptographicKeyBinding(VCIssuanceContext vcIssuanceContext){ | ||
| // Make sure we are dealing with a jwk proof. | ||
| if (vcIssuanceContext.getCredentialConfig().getCryptographicBindingMethodsSupported() == null || | ||
| !vcIssuanceContext.getCredentialConfig().getCryptographicBindingMethodsSupported().contains(CRYPTOGRAPHIC_BINDING_METHOD_JWK)) { | ||
| throw new IllegalStateException("This SD-JWT implementation only supports jwk as cryptographic binding method"); | ||
| } | ||
| } | ||
|
|
||
| private Optional<Proof> getProofFromContext(VCIssuanceContext vcIssuanceContext) throws VCIssuerException { | ||
| return Optional.ofNullable(vcIssuanceContext.getCredentialConfig()) | ||
| .map(SupportedCredentialConfiguration::getProofTypesSupported) | ||
| .flatMap(proofTypesSupported -> { | ||
| Optional.ofNullable(proofTypesSupported.getJwt()) | ||
| .orElseThrow(() -> new VCIssuerException("SD-JWT supports only jwt proof type.")); | ||
|
|
||
| Proof proof = Optional.ofNullable(vcIssuanceContext.getCredentialRequest().getProof()) | ||
| .orElseThrow(() -> new VCIssuerException("Credential configuration requires a proof of type: " + ProofType.JWT)); | ||
|
|
||
| if (!Objects.equals(proof.getProofType(), ProofType.JWT)) { | ||
| throw new VCIssuerException("Wrong proof type"); | ||
| } | ||
|
|
||
| return Optional.of(proof); | ||
| }); | ||
| } | ||
|
|
||
| private JWSInput getJwsInput(Proof proof) throws JWSInputException { | ||
| return new JWSInput(proof.getJwt()); | ||
| } | ||
|
|
||
| /** | ||
| * As we limit accepted algorithm to the ones listed by the issuer, we can omit checking for "none" | ||
| * The Algorithm enum class does not list the none value anyway. | ||
| * | ||
| * @param vcIssuanceContext | ||
| * @param jwsHeader | ||
| * @throws VCIssuerException | ||
| */ | ||
| private void validateJwsHeader(VCIssuanceContext vcIssuanceContext, JWSHeader jwsHeader) throws VCIssuerException { | ||
| Optional.ofNullable(jwsHeader.getAlgorithm()) | ||
| .orElseThrow(() -> new VCIssuerException("Missing jwsHeader claim alg")); | ||
|
|
||
| // As we limit accepted algorithm to the ones listed by the server, we can omit checking for "none" | ||
| // The Algorithm enum class does not list the none value anyway. | ||
| Optional.ofNullable(vcIssuanceContext.getCredentialConfig()) | ||
| .map(SupportedCredentialConfiguration::getProofTypesSupported) | ||
| .map(ProofTypesSupported::getJwt) | ||
| .map(ProofTypeJWT::getProofSigningAlgValuesSupported) | ||
| .filter(supportedAlgs -> supportedAlgs.contains(jwsHeader.getAlgorithm().name())) | ||
| .orElseThrow(() -> new VCIssuerException("Proof signature algorithm not supported: " + jwsHeader.getAlgorithm().name())); | ||
|
|
||
| Optional.ofNullable(jwsHeader.getType()) | ||
| .filter(type -> Objects.equals(PROOF_JWT_TYP, type)) | ||
| .orElseThrow(() -> new VCIssuerException("JWT type must be: " + PROOF_JWT_TYP)); | ||
|
|
||
| // KeyId shall not be present alongside the jwk. | ||
| Optional.ofNullable(jwsHeader.getKeyId()) | ||
| .ifPresent(keyId -> { | ||
| throw new VCIssuerException("KeyId not expected in this JWT. Use the jwk claim instead."); | ||
| }); | ||
| } | ||
|
|
||
| private void validateProofPayload(VCIssuanceContext vcIssuanceContext, AccessToken proofPayload) throws VCIssuerException { | ||
| // azp is the id of the client, as mentioned in the access token used to request the credential. | ||
| // Token provided from user is obtained with a clientId that support the oidc login protocol. | ||
| // oid4vci client doesn't. But it is the client needed at the credential endpoint. | ||
| // String azp = vcIssuanceContext.getAuthResult().getToken().getIssuedFor(); | ||
| // Optional.ofNullable(proofPayload.getIssuer()) | ||
| // .filter(proofIssuer -> Objects.equals(azp, proofIssuer)) | ||
| // .orElseThrow(() -> new VCIssuerException("Issuer claim must be null for preauthorized code else the clientId of the client making the request: " + azp)); | ||
|
|
||
francis-pouatcha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // The issuer is the token / credential is the audience of the proof | ||
| String credentialIssuer = vcIssuanceContext.getVerifiableCredential().getIssuer().toString(); | ||
| Optional.ofNullable(proofPayload.getAudience()) // Ensure null-safety with Optional | ||
| .map(Arrays::asList) // Convert to List<String> | ||
| .filter(audiences -> audiences.contains(credentialIssuer)) // Check if the issuer is in the audience list | ||
| .orElseThrow(() -> new VCIssuerException( | ||
| "Proof not produced for this audience. Audience claim must be: " + credentialIssuer + " but are " + Arrays.asList(proofPayload.getAudience()))); | ||
|
|
||
| // Validate mandatory iat. | ||
| // I do not understand the rationale behind requiring an issue time if we are not checking expiration. | ||
| Optional.ofNullable(proofPayload.getIat()) | ||
| .orElseThrow(() -> new VCIssuerException("Missing proof issuing time. iat claim must be provided.")); | ||
|
|
||
| // Check cNonce matches. | ||
| // If the token endpoint provides a c_nonce, we would like this: | ||
| // - stored in the access token | ||
| // - having the same validity as the access token. | ||
| Optional.ofNullable(vcIssuanceContext.getAuthResult().getToken().getNonce()) | ||
| .ifPresent( | ||
| cNonce -> { | ||
| Optional.ofNullable(proofPayload.getNonce()) | ||
| .filter(nonce -> Objects.equals(cNonce, nonce)) | ||
| .orElseThrow(() -> new VCIssuerException("Missing or wrong nonce value. Please provide nonce returned by the issuer if any.")); | ||
|
|
||
| // We expect the expiration to be identical to the token expiration. We assume token expiration has been checked by AuthManager, | ||
| // So no_op | ||
| } | ||
| ); | ||
|
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.