[java-oidc-common] 28/35: JCOMOIDC-45 - Add a Decrypter for JWE tokens similar to the opensaml Decrypter

Phil Smart philip.smart at jisc.ac.uk
Tue Sep 20 14:19:28 UTC 2022


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch dev/JCOMOIDC-41
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=fee5acb7c697fbc3560ad34f7fad5dfe637f6b49

commit fee5acb7c697fbc3560ad34f7fad5dfe637f6b49
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Aug 23 15:58:28 2022 +0100

    JCOMOIDC-45 - Add a Decrypter for JWE tokens similar to the opensaml
    Decrypter
    
     - Updates to the basic JOSE Header credential resolver and the local
    credential resolver. Includes local credential resolution based on a
    public key match to that described in the 'jwk' in the JOSE header - if
    there is one.
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-45
---
 .../impl/BasicJOSEObjectCredentialResolver.java    |  42 +++-
 .../impl/CriterionCredentialResolver.java          |   4 +-
 .../impl/LocalJOSEObjectCredentialResolver.java    | 163 ++++++++++++-
 .../oidc/security/impl/JWTDecrypter.java           |   9 -
 .../LocalJOSEObjectCredentialResolverTest.java     | 254 +++++++++++++++++++++
 5 files changed, 451 insertions(+), 21 deletions(-)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/BasicJOSEObjectCredentialResolver.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/BasicJOSEObjectCredentialResolver.java
index b4d4d76..1dae249 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/BasicJOSEObjectCredentialResolver.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/BasicJOSEObjectCredentialResolver.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.impl.AbstractCriteriaFilteringCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoResolutionContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -73,25 +74,53 @@ public class BasicJOSEObjectCredentialResolver extends AbstractCriteriaFiltering
              throw new ResolverException("JOSEObjectCriterion did not contain an instance of JOSEObject");
         }
         
+        // This will be the list of credentials to return.
+        List<Credential> credentials = null;
+        
         final Header header = joseObject.getHeader();
         if (JWSHeader.class.isInstance(header)) {
-            return processJWSHeader(JWSHeader.class.cast(header));
+            credentials = processJWSHeader(JWSHeader.class.cast(header));
         } else if (JWEHeader.class.isInstance(header)) {
-            return processJWEHeader(JWEHeader.class.cast(header));
+            credentials = processJWEHeader(JWEHeader.class.cast(header));           
         } else {
             throw new ResolverException("Saw unknown JOSEObject header type: " +
                     header != null ? header.getClass().getName() : "null");
         }
         
+        // Extention point for subclasses
+        postProcess(criteriaSet, joseObject, credentials);
+        
+        log.debug("A total of {} credentials were resolved", credentials.size());
+        
+        return credentials;
+        
+    }
+    
+    /**
+     * Hook for subclasses to do post-processing of the credential set after all JOSE header keys have been processed.
+     * 
+     * For example, the previously resolved credentials might be used to index into a store of local credentials, where
+     * the index is a key name or the public half of a key pair extracted from the headers.
+     * 
+     * @param criteriaSet the credential criteria used to resolve credentials
+     * @param joseObject the extracted JOSE object
+     * @param credentials the list which will store the resolved credentials
+     * 
+     * @throws ResolverException thrown if there is an error during processing
+     */
+    protected void postProcess(@Nullable final CriteriaSet criteriaSet, @Nonnull final JOSEObject joseObject,
+            @Nonnull final List<Credential> credentials)
+            throws ResolverException {
+
     }
 
     /**
      * Process credentials indicated by a JWS header.
      * 
      * @param jwsHeader the JWS header to process
-     * @return 
+     * @return the list of credentials specified by the JWS header
      */
-    @Nonnull @NonnullElements protected Iterable<Credential> processJWSHeader(@Nonnull final JWSHeader jwsHeader) {
+    @Nonnull @NonnullElements protected List<Credential> processJWSHeader(@Nonnull final JWSHeader jwsHeader) {
         final List<Credential> credentials = new ArrayList<>();
         
         // JWK
@@ -108,7 +137,7 @@ public class BasicJOSEObjectCredentialResolver extends AbstractCriteriaFiltering
         return credentials;
     }
 
-    @Nonnull @NonnullElements protected Iterable<Credential> processJWEHeader(@Nonnull final JWEHeader jweHeader) {
+    @Nonnull @NonnullElements protected List<Credential> processJWEHeader(@Nonnull final JWEHeader jweHeader) {
         final List<Credential> credentials = new ArrayList<>();
         
         // JWK
@@ -144,6 +173,9 @@ public class BasicJOSEObjectCredentialResolver extends AbstractCriteriaFiltering
             credential.getKeyNames().add(jwk.getKeyID());
             credential.setKid(jwk.getKeyID());
         }
+        if (jwk.getAlgorithm() != null) {
+            credential.setAlgorithm(jwk.getAlgorithm());
+        }
         if (headerKid != null && !headerKid.equals(credential.getKid())) {
             log.warn("Key ID in JOSE header does not match 'kid' in JWK");
             return null;
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/CriterionCredentialResolver.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/CriterionCredentialResolver.java
index 35229ce..64b7db0 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/CriterionCredentialResolver.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/CriterionCredentialResolver.java
@@ -32,11 +32,9 @@ import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
 /**
- * Extracts the credential from the {@link StaticCredentialCriterion} inside the given criteria set,
- * inspects its suitability and passes it back.
+ * Extracts the credential from the {@link StaticCredentialCriterion} inside the given criteria set.
  */
 //TODO Docs
-//TODO Algorithm filters?
 public class CriterionCredentialResolver extends BasicJOSEObjectCredentialResolver {
     
     /** Class logger. */
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolver.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolver.java
index f24bc2f..7f11e47 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolver.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolver.java
@@ -17,16 +17,30 @@
 
 package net.shibboleth.oidc.security.credential.impl;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.CredentialResolver;
+import org.opensaml.security.criteria.PublicKeyCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Predicates;
+import com.nimbusds.jose.Header;
+import com.nimbusds.jose.JOSEObject;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWSHeader;
+
 import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.impl.JWTDecrypter;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
@@ -35,9 +49,37 @@ import net.shibboleth.utilities.java.support.resolver.ResolverException;
  * A simple specialization of {@link BasicJOSEObjectCredentialResolver}
  * which is capable of using resolving local credentials from a supplied {@link CredentialResolver} 
  * which manages local credentials.
+ * 
+ * <p>
+ * The local credential resolver supplied should manage and return credentials
+ * which contain either a secret (symmetric) key or the private key half of a
+ * key pair.
+ * </p>
+ * 
+ * <p>
+ * A typical use case for this class would be as a resolver of decryption keys,
+ * such as is needed by {@link JWTDecrypter}}.
+ * </p>
+ * 
+ * <p>
+ * Resolution proceeds as follows:
+ * </p>
+ * <ol>
+ *   <li>Any credential resolved via the standard {@link BasicJOSEObjectCredentialResolver}
+ *       resolution process which is a local credential (contains private key) will be removed
+ *       from the effective set of credentials to be returned. </li>
+ *   <li>If a credential so removed contained a public key, that key will be used as a
+ *       resolution criteria input to the local credential resolver (along with the keyID 'kid' 
+ *       if one exists in the JOSE headers).  Any local credentials
+ *       so resolved will be added to the set to be returned.</li>
+ *   <li>Similarly, the keyID 'kid' from the JOSE headers will also
+ *       be used as resolution criteria for local credentials and the resultant credentials
+ *       added to the set to be returned providing they do not share the same private and public key
+ *       pair as one already resolved - avoiding key duplication in the result.</li>
+ * </ol>
+ * Adds either 'kid' or 'public key' to the criteria set sent to the local credential resolver for resolution.
+ * 
  */
-// TODO use the 'public' credentials on the JOSE Object headers to locate local private keys with in addition to 
-// those located by 'kid'
 public class LocalJOSEObjectCredentialResolver extends BasicJOSEObjectCredentialResolver {
     
     /** Class logger. */
@@ -69,9 +111,122 @@ public class LocalJOSEObjectCredentialResolver extends BasicJOSEObjectCredential
     }
     
     @Override
-    @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
+    @Nonnull protected void postProcess(@Nullable final CriteriaSet criteriaSet, 
+            @Nonnull final JOSEObject joseObject, @Nonnull final List<Credential> credentials)
             throws ResolverException {        
-        return getLocalCredentialResolver().resolve(criteriaSet);        
+        
+        final List<Credential> results = new ArrayList<>();
+        
+        final String kid = resolveKeyIdFromJoseHeader(joseObject.getHeader());
+        
+        for (final Credential inputCred : credentials) {
+            if (isLocalCredential(inputCred)) {
+                log.debug("Input credential was local, including in results");
+                results.add(inputCred);
+            } else if (inputCred.getPublicKey() != null) {
+                
+                final CriteriaSet criteria = new CriteriaSet();              
+                // Add public key criterion
+                criteria.add(new PublicKeyCriterion(inputCred.getPublicKey()));   
+                
+                if (kid != null) {
+                    // Also filter public key by keyID if one exists in the 'kid' parameter.
+                    // This is also caught upstream, so add in-case resolver needs it and filter
+                    // anyway.
+                    criteria.add(new EvaluableKeyIDCredentialCriterion(kid)); 
+                }
+                final List<Credential> localCredentials = resolveLocalCredentialsByCriteria(criteria);
+                log.trace("Matched {} local credential(s) from {} input credential(s) on 'public key' and optionally "
+                        + "'kid' criterion", localCredentials.size(), credentials.size());
+                if (!localCredentials.isEmpty()) {
+                    results.addAll(localCredentials);
+                }
+            }
+        }
+        
+        // If 'kid' exists in the header, also resolve by kid (in-case there were no input credentials)
+        if (kid != null) {
+            final CriteriaSet criteria = new CriteriaSet();
+            criteria.add(new EvaluableKeyIDCredentialCriterion(kid));
+            final List<Credential> localCredentials = resolveLocalCredentialsByCriteria(criteria);
+            log.trace("Found {} credential(s) from 'kid' criterion alone", localCredentials.size());
+            // There is no point in adding a duplicate credential, so filter out those already resolved
+            results.addAll(filterAlreadyContained(results, localCredentials));
+        }
+        
+        credentials.clear();
+        credentials.addAll(results);
+    }
+    
+    /**
+     * Return a new list of credentials based on the {@code credentialsToFilter} that are not contained in 
+     * {@code credentialsToFilterOn}. Containment is determined by equality of both private and public keys. 
+     * 
+     * @param credentialsToFilterOn the credentials used to filter the {@code credentialsToFilter}
+     * @param credentialsToFilter the credentials which will be filtered
+     * 
+     * @return credentials contained in {@code credentialsToFilter} not in {@code credentialsToFilterOn}.
+     */
+    @Nonnull @NonnullElements @Live private List<Credential> filterAlreadyContained(
+            @Nonnull final List<Credential> credentialsToFilterOn, @Nonnull final List<Credential> credentialsToFilter){
+        
+        return credentialsToFilter.stream().filter(Predicates.not(lc -> credentialsToFilterOn.stream()
+                .anyMatch(r -> lc.getPrivateKey().equals(r.getPrivateKey()) 
+                        && lc.getPublicKey().equals(r.getPublicKey()))))
+        .collect(Collectors.toList());
+        
+    }
+    
+    /**
+     * Resolve credentials using the {@link #localCredResolver} and the supplied criteria.
+     * 
+     * @param criteriaSet the criterion to pass to the credential resolver
+     * 
+     * @return collection of local credentials identified by the criteria
+     * 
+     * @throws ResolverException thrown if there is a problem resolving credentials from the 
+     *          local credential resolver
+     */
+    @Nonnull @NonnullElements @Live private List<Credential> resolveLocalCredentialsByCriteria(
+            final CriteriaSet criteriaSet) throws ResolverException{
+        
+        final ArrayList<Credential> localCreds = new ArrayList<>();
+        
+        for (final Credential cred : getLocalCredentialResolver().resolve(criteriaSet)) {
+            if (isLocalCredential(cred)) {
+                localCreds.add(cred);
+            }
+        }        
+        return localCreds;
+    }
+    
+    /**
+     * Return the KeyId from either a {@link JWSHeader} or a {@link JWEHeader}. Returns
+     * {@literal null} if not found.
+     * 
+     * @param header the JOSE header to find a kid from
+     * 
+     * @return the keyId or null
+     */
+    @Nullable private String resolveKeyIdFromJoseHeader(@Nonnull final Header header) {
+        if (header instanceof JWEHeader) {
+            return ((JWEHeader)header).getKeyID();
+        } else if (header instanceof JWSHeader) {
+            return ((JWSHeader)header).getKeyID();
+        }
+        return null;
+    }
+    
+    /**
+     * Determine whether the credential is a local credential.
+     * 
+     * A local credential will have either a private key or a secret (symmetric) key.
+     * 
+     * @param credential the credential to evaluate
+     * @return true if the credential has either a private or secret key, false otherwise
+     */
+    protected boolean isLocalCredential(@Nonnull final Credential credential) {
+        return credential.getPrivateKey() != null || credential.getSecretKey() != null;
     }
 
 }
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTDecrypter.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTDecrypter.java
index 2fb158a..a199551 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTDecrypter.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTDecrypter.java
@@ -67,7 +67,6 @@ import com.nimbusds.jwt.JWTParser;
 
 import net.shibboleth.oidc.security.JWTDecryptionParameters;
 import net.shibboleth.oidc.security.credential.JWKCredential;
-import net.shibboleth.oidc.security.credential.impl.EvaluableKeyIDCredentialCriterion;
 import net.shibboleth.oidc.security.criterion.JOSEObjectCriterion;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
@@ -177,14 +176,6 @@ public class JWTDecrypter {
         // Add the entire object so the resolver can access it
         newCriteriaSet.add(new JOSEObjectCriterion(encryptedObject));
 
-        // If 'kid' exists in the header, create an EvaluableKeyID criterion
-        if (encryptedObject.getHeader().getKeyID() != null) {
-            // FIXME: This is created directly as an EvaluableCriterion as I can not see a way to 
-            // add to the default mappings in EvaluableCredentialCriteriaRegistry without overriding the opensaml
-            // version. There is a hook for this, to add your own
-            newCriteriaSet.add(new EvaluableKeyIDCredentialCriterion(encryptedObject.getHeader().getKeyID()));
-        }
-        
         return newCriteriaSet;
     }
     
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolverTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolverTest.java
new file mode 100644
index 0000000..39fd646
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/credential/impl/LocalJOSEObjectCredentialResolverTest.java
@@ -0,0 +1,254 @@
+package net.shibboleth.oidc.security.credential.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.impl.AbstractCriteriaFilteringCredentialResolver;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.criterion.JOSEObjectCriterion;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/** Tests for {@link LocalJOSEObjectCredentialResolver}.*/
+public class LocalJOSEObjectCredentialResolverTest {
+    
+    private static final String CLIENT_SECRET = "Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$";
+    
+    private LocalJOSEObjectCredentialResolver resolver;
+
+    /** The key resolved from the local credentials source.*/
+    private RSAKey localRSAKey;
+    
+    
+    @BeforeMethod
+    public void setup() throws Exception {
+        
+        localRSAKey = new RSAKeyGenerator(2048)
+                .algorithm(JWEAlgorithm.RSA_OAEP_256)
+                .keyUse(KeyUse.ENCRYPTION)
+                .keyID("mock-key")
+                .generate();
+        
+        resolver = new LocalJOSEObjectCredentialResolver(
+                new MockRSACriteriaFilteringCredentialResolver(localRSAKey));
+    }
+    
+    private SignedJWT createdSignedJWT() throws KeyLengthException, JOSEException {        
+        final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+                .type(JOSEObjectType.JWT)
+                .keyID("mock-key")
+                .build();       
+        final var signedJWT = new SignedJWT(header,createClaims());
+        signedJWT.sign(new MACSigner(CLIENT_SECRET));
+        return signedJWT;
+    }
+    
+    private JWTClaimsSet createClaims() {
+        return new JWTClaimsSet.Builder()
+                .issuer("https://localhost:9918")
+                .audience(List.of("test-client"))
+                .subject("jdoe")
+                .claim("nonce", "abadnonce")
+                .claim("azp", "test-client")
+                .claim("name","jdoe")
+                .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+                .build();
+    }
+    
+    /* Resolve a credential using the public key found in the JOSEHeaders. The public component of
+     * the JWK in the header is different than the one resolved locally.*/
+    @Test
+    public void testSuccessful_PublicKeyInJOSEHeaderMatchesLocal() throws Exception {
+        
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                // No KeyID, so only has public key to resolve on
+                .jwk(localRSAKey.toPublicJWK())
+                .build(),
+                new Payload(createdSignedJWT()));
+        jweObject.encrypt(new RSAEncrypter(localRSAKey));
+        final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+        System.out.println(jwe.serialize());
+        
+        final CriteriaSet criteria = new CriteriaSet(new JOSEObjectCriterion(jwe));
+        final Credential resolvedCredential = resolver.resolveSingle(criteria);
+        assertNotNull(resolvedCredential);
+        assertNotNull(resolvedCredential.getPrivateKey());
+        assertTrue(resolvedCredential.getKeyNames().contains("mock-key"));
+    }
+    
+    /* Resolve a credential using the public key found in the JOSEHeaders. The public component of
+     * the JWK in the header is different than the one resolved locally.*/
+    @Test
+    public void testUnsuccessful_PublicKeyInJOSEHeaderDoesNotMatchLocal() throws Exception {
+        final RSAKey keyInJoseHeader = new RSAKeyGenerator(2048)
+                .algorithm(JWEAlgorithm.RSA_OAEP_256)
+                .keyUse(KeyUse.ENCRYPTION)
+                .keyID("enc-key")
+                .generate();
+        
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                .keyID("enc-key")
+                .jwk(keyInJoseHeader.toPublicJWK())
+                .build(),
+                new Payload(createdSignedJWT()));
+        jweObject.encrypt(new RSAEncrypter(keyInJoseHeader));
+        final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+        System.out.println(jwe.serialize());
+        
+        final CriteriaSet criteria = new CriteriaSet(new JOSEObjectCriterion(jwe));
+        final Credential resolvedCredential = resolver.resolveSingle(criteria);
+        assertNull(resolvedCredential);
+    }
+    
+    /* Just 'kid' no 'jwk'.*/
+    @Test
+    public void testSuccessful_KeyIDInJOSEHeader() throws Exception {
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                .keyID("mock-key")
+                .build(),
+                new Payload(createdSignedJWT()));
+        jweObject.encrypt(new RSAEncrypter(localRSAKey));
+        final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+        System.out.println(jwe.serialize());
+        
+        final CriteriaSet criteria = new CriteriaSet(new JOSEObjectCriterion(jwe));
+        final List<Credential> resolvedCredential = StreamSupport.stream(
+                resolver.resolve(criteria).spliterator(), false).collect(Collectors.toList());
+        assertNotNull(resolvedCredential);
+        assertEquals(resolvedCredential.size(),1);
+        assertNotNull(resolvedCredential.get(0).getPrivateKey());
+        assertTrue(resolvedCredential.get(0).getKeyNames().contains("mock-key"));
+    }
+    
+    /* Just 'kid' no 'jwk'.*/
+    @Test
+    public void testUnSuccessful_KeyIDInJOSEHeaderDifferentThanLocalCred() throws Exception {
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                .keyID("different-than-local-cred")
+                .build(),
+                new Payload(createdSignedJWT()));
+        jweObject.encrypt(new RSAEncrypter(localRSAKey));
+        final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+        System.out.println(jwe.serialize());
+        
+        final CriteriaSet criteria = new CriteriaSet(new JOSEObjectCriterion(jwe));
+        final Credential resolvedCredential = resolver.resolveSingle(criteria);
+        assertNull(resolvedCredential);     
+    }
+    
+    //TODO test kid in header different than JWK should match? (https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4)
+    
+    /* There is a 'kid' in the header and a 'jwk', the 'kid' matches the key. Resolve the key once.*/
+    @Test
+    public void testSuccessful_KeyIDInJOSEHeader_And_JWK() throws Exception {
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                .keyID("mock-key")
+                .jwk(localRSAKey.toPublicJWK())
+                .build(),
+                new Payload(createdSignedJWT()));
+        jweObject.encrypt(new RSAEncrypter(localRSAKey));
+        final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+        System.out.println(jwe.serialize());
+        
+        final CriteriaSet criteria = new CriteriaSet(new JOSEObjectCriterion(jwe));
+        final List<Credential> resolvedCredential = StreamSupport.stream(
+                resolver.resolve(criteria).spliterator(), false).collect(Collectors.toList());
+        assertNotNull(resolvedCredential);
+        assertEquals(resolvedCredential.size(),1);
+        assertNotNull(resolvedCredential.get(0).getPrivateKey());
+        assertTrue(resolvedCredential.get(0).getKeyNames().contains("mock-key"));
+    }
+    
+    /* There is a 'kid' in the header and 'jwk', and the 'kid' is different than the kid of the 'jwk'.*/
+    @Test
+    public void testUnsuccessful_KeyIDInJOSEHeader_And_JWK_KidDoesNotMatch() throws Exception {
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                .keyID("different-than-jwk")
+                .jwk(localRSAKey.toPublicJWK())
+                .build(),
+                new Payload(createdSignedJWT()));
+        jweObject.encrypt(new RSAEncrypter(localRSAKey));
+        final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+        System.out.println(jwe.serialize());
+        
+        final CriteriaSet criteria = new CriteriaSet(new JOSEObjectCriterion(jwe));
+        final List<Credential> resolvedCredential = StreamSupport.stream(
+                resolver.resolve(criteria).spliterator(), false).collect(Collectors.toList());
+        assertNotNull(resolvedCredential);
+        assertEquals(resolvedCredential.size(),0);
+    }
+    
+    /** Mock RSA credential resolver that is filterable.*/
+    private static class MockRSACriteriaFilteringCredentialResolver extends AbstractCriteriaFilteringCredentialResolver
+        implements JOSEObjectCredentialResolver {
+    
+        private final RSAKey key;
+    
+        public MockRSACriteriaFilteringCredentialResolver(final RSAKey theKey) {
+            key = theKey;
+        }
+
+        @Override
+        protected Iterable<Credential> resolveFromSource(final CriteriaSet criteriaSet) throws ResolverException {
+            final BasicJWKCredential jwkCredential = new BasicJWKCredential();
+            jwkCredential.setAlgorithm(JWEAlgorithm.RSA_OAEP_256);
+            jwkCredential.getKeyNames().add("mock-key");
+            jwkCredential.setKid("mock-key");
+            try {
+                jwkCredential.setPrivateKey(key.toPrivateKey());
+                jwkCredential.setPublicKey(key.toPublicKey());
+            } catch (final JOSEException e) {
+                Assert.fail();
+            }
+            
+            return List.of(jwkCredential);
+        }
+    
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list