[java-oidc-common] branch dev/JCOMOIDC-41 updated: Cleanup direct and key wrapping decryption

Phil Smart philip.smart at jisc.ac.uk
Mon Sep 26 16:55:34 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=34d2ad0c6a5ccd868daec74c602726293d0c9d3d

The following commit(s) were added to refs/heads/dev/JCOMOIDC-41 by this push:
     new 34d2ad0  Cleanup direct and key wrapping decryption
34d2ad0 is described below

commit 34d2ad0c6a5ccd868daec74c602726293d0c9d3d
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Sep 26 17:55:28 2022 +0100

    Cleanup direct and key wrapping decryption
---
 .../DefaultJWTEncryptionParametersResolver.java    | 41 +++---------
 .../oidc/security/impl/JWKCredentialSupport.java   | 66 +++++++++++++++++++
 .../oidc/security/impl/JWTDecrypter.java           | 75 ++++++++++++++++------
 3 files changed, 130 insertions(+), 52 deletions(-)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/DefaultJWTEncryptionParametersResolver.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/DefaultJWTEncryptionParametersResolver.java
index 980054e..f7564c3 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/DefaultJWTEncryptionParametersResolver.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/DefaultJWTEncryptionParametersResolver.java
@@ -28,7 +28,6 @@ import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.crypto.SecretKey;
 
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.CredentialResolver;
@@ -49,9 +48,6 @@ import com.nimbusds.jose.Algorithm;
 import com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWEAlgorithm;
-import com.nimbusds.jose.util.StandardCharset;
-import com.nimbusds.oauth2.sdk.auth.Secret;
-import com.nimbusds.oauth2.sdk.jose.SecretKeyDerivation;
 
 import net.shibboleth.oidc.security.JWTEncryptionConfiguration;
 import net.shibboleth.oidc.security.JWTEncryptionParameters;
@@ -490,50 +486,29 @@ public class DefaultJWTEncryptionParametersResolver extends AbstractSecurityPara
     }
     
     /**
-     * Derive a *new* symmetric key credential suitable for the given 'alg' and 'enc' algorithms. The
-     * new credential is a copy of the given credential but replacing the keyID and secret. If any of 
-     * the inputs are {@code null} then {@code null} is returned. 
+     * Wrapper to 
+     * {@link JWKCredentialSupport#deriveSymmetricKeyForAlgAndEnc(JWKCredential, JWEAlgorithm, EncryptionMethod)}
+     * which derives a *new* symmetric key credential suitable for the given 'alg' and 'enc' algorithms. Catches 
+     * and logs errors, returning {@literal null} on error.
      * 
      * @param credential the credential that contains a secret key to derive a new credential from
      * @param alg the key management mode algorithm
      * @param enc the content encryption algorithm
      * 
      * @return a *new* derived credential, or {@code null} if an error occurs
+     * @throws JOSEException on error deriving the credential
      */
     @Nullable private JWKCredential deriveSymmetricKeyForAlgAndEnc(
-            @Nullable final JWKCredential credential, @Nullable final JWEAlgorithm alg, @Nullable final EncryptionMethod enc) {
+            @Nullable final JWKCredential credential, @Nullable final JWEAlgorithm alg, 
+            @Nullable final EncryptionMethod enc) {
         
-        if (credential == null || credential.getSecretKey() == null || alg == null || enc == null) {
-            return null;
-        }
         try {
-            final SecretKey derivedKey = generateSymmetricKey(credential.getSecretKey().getEncoded(), alg, enc);
-            //build a new credential so the old is left in its original state
-            return CredentialConversionUtil.copySymmetricCredentialWithNewSecret(
-                    credential, "derived-"+CredentialConversionUtil.resolveKid(credential), derivedKey);
+            return JWKCredentialSupport.deriveSymmetricKeyForAlgAndEnc(credential, alg, enc);
         } catch (final JOSEException e) {
             log.warn("Unable to generate secret key: {}", e.getMessage());
             return null;
         }
-    }
-   
     
-    /**
-     * Generate symmetric key from client_secret using the algorithms supplied.
-     * 
-     * @param clientSecret client secret to derive a key from
-     * @param alg the key management mode or key transport algorithm
-     * @param enc the content encryption algorithm
-     * 
-     * @return key derived from client secret.
-
-     * @throws JOSEException on error 
-     */
-    private final SecretKey generateSymmetricKey(final byte[] clientSecret, final JWEAlgorithm alg,
-            final EncryptionMethod enc) throws JOSEException {
-        
-        return SecretKeyDerivation.deriveSecretKey(
-                new Secret(new String(clientSecret, StandardCharset.UTF_8)), alg, enc);        
     }
     
 
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWKCredentialSupport.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWKCredentialSupport.java
new file mode 100644
index 0000000..359f8e6
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWKCredentialSupport.java
@@ -0,0 +1,66 @@
+package net.shibboleth.oidc.security.impl;
+
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.util.StandardCharset;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.jose.SecretKeyDerivation;
+
+import net.shibboleth.oidc.security.credential.JWKCredential;
+
+/** Provide JWK specific credential support.*/
+public class JWKCredentialSupport {
+    
+    /** Constructor. */
+    private JWKCredentialSupport() { }
+    
+    
+    /**
+     * Derive a *new* symmetric key credential suitable for the given 'alg' and 'enc' algorithms. The
+     * new credential is a copy of the given credential but replacing the keyID and secret. If any of 
+     * the inputs are {@code null} then {@code null} is returned. 
+     * 
+     * @param credential the credential that contains a secret key to derive a new credential from
+     * @param alg the key management mode algorithm
+     * @param enc the content encryption algorithm
+     * 
+     * @return a *new* derived credential, or {@code null} if an error occurs
+     * @throws JOSEException on error deriving the credential
+     */
+    @Nullable public static final JWKCredential deriveSymmetricKeyForAlgAndEnc(
+            @Nullable final JWKCredential credential, @Nullable final JWEAlgorithm alg, 
+            @Nullable final EncryptionMethod enc) throws JOSEException {
+        
+        if (credential == null || credential.getSecretKey() == null || alg == null || enc == null) {
+            return null;
+        }
+        final SecretKey derivedKey = generateSymmetricKey(credential.getSecretKey().getEncoded(), alg, enc);
+        //build a new credential so the old is left in its original state
+        return CredentialConversionUtil.copySymmetricCredentialWithNewSecret(
+                credential, "derived-"+CredentialConversionUtil.resolveKid(credential), derivedKey);
+    }
+   
+    
+    /**
+     * Generate symmetric key from client_secret using the algorithms supplied.
+     * 
+     * @param clientSecret client secret to derive a key from
+     * @param alg the key management mode or key transport algorithm
+     * @param enc the content encryption algorithm
+     * 
+     * @return key derived from client secret.
+
+     * @throws JOSEException on error 
+     */
+    public static final SecretKey generateSymmetricKey(final byte[] clientSecret, final JWEAlgorithm alg,
+            final EncryptionMethod enc) throws JOSEException {
+        
+        return SecretKeyDerivation.deriveSecretKey(
+                new Secret(new String(clientSecret, StandardCharset.UTF_8)), alg, enc);        
+    }
+
+}
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 dfe4448..4f0275c 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
@@ -79,6 +79,9 @@ import net.shibboleth.utilities.java.support.resolver.ResolverException;
  * The {@link EncryptedJWT} will be decrypted in-place, with its {@link State} changing to {@link State#DECRYPTED}
  * on successful decryption. Any error that occurs decrypting the JWT will throw an {@link DecryptionException}.
  * 
+ * <p>Note, as per OIDC specification, symmetric decryption keys are derived from resolved symmetric keys 
+ * (e.g. a client_secret).</p>
+ * 
  * <p>A decrypter should be created for each new decryption operation.</p>
  */
 public class JWTDecrypter {
@@ -344,8 +347,8 @@ public class JWTDecrypter {
 
     /**
      * Decrypt the encrypted JWT by first decrypting the wrapped content encryption key using on of the shared 
-     * (symmetric) key encryption keys resolved by the {@link JWTDecryptionParameters#getKEKCredentialResolver()} resolver. 
-     * 
+     * key wrapping keys derived from a symmetric key resolved by the 
+     * {@link JWTDecryptionParameters#getKEKCredentialResolver()} resolver. 
      * 
      * <p>For each credential, algorithm compatibly is checked against that described by the JWE and any
      * includes and excludes lists configured.</p>
@@ -369,9 +372,22 @@ public class JWTDecrypter {
         
         try {
             for (final Credential cred : params.getKEKCredentialResolver().resolve(criteria)) {
+                if (!(cred instanceof JWKCredential)) {
+                    if (log.isTraceEnabled()) {
+                        log.trace("JWT decryption requires a JWK credential, resolved credential '{}' was not", 
+                                CredentialConversionUtil.resolveKid(cred));
+                    }
+                    // Require a JWKCredential
+                    continue;
+                }
                 try {
-                    validateKeyManagmentAlgorithm(encryptedObject, cred);
-                    final JWEDecrypter decrypter = new AESDecrypter(cred.getSecretKey());
+                    final JWEAlgorithm alg = validateKeyManagmentAlgorithm(encryptedObject, cred);
+                    final EncryptionMethod enc = validateContentEncryptionAlgorithm(encryptedObject, cred);
+                    //Key wrapping requires a derived credential
+                    final JWKCredential derivedKey = 
+                            JWKCredentialSupport.deriveSymmetricKeyForAlgAndEnc((JWKCredential)cred, 
+                                    alg, enc);
+                    final JWEDecrypter decrypter = new AESDecrypter(derivedKey.getSecretKey());
                     encryptedObject.decrypt(decrypter);
                     return;
                 } catch (final JOSEException | DecryptionException e) {
@@ -429,14 +445,15 @@ public class JWTDecrypter {
     }
 
     /**
-     * Decrypt the encrypted JWT using direct encryption. A shared symmetric key resolved by the
-     * {@link JWTDecryptionParameters#getContentEncryptionKeyCredentialResolver()} is used directly
-     * as the content encryption key.
+     * Decrypt the encrypted JWT using direct encryption. One of the shared symmetric keys resolved by the
+     * {@link JWTDecryptionParameters#getContentEncryptionKeyCredentialResolver()} is used to derive
+     * a content encryption key to decrypt the JWT - if any are appropriate.
      * 
      * <p>For each credential, algorithm compatibly is checked against that described by the JWE and any
      * includes and excludes lists configured.</p>
      * 
-     * <p>The first resolved credential that decrypts the JWT produces a result, and the process terminates.</p>
+     * <p>The first resolved (and derived) credential that decrypts the JWT produces a result, 
+     * and the process terminates.</p>
      * 
      * @param encryptedObject the encrypted JWT to decrypt - in place.
      * 
@@ -458,10 +475,22 @@ public class JWTDecrypter {
         
         try {
             for (final Credential cred : params.getContentEncryptionKeyCredentialResolver().resolve(criteria)) {
+                if (!(cred instanceof JWKCredential)) {
+                    if (log.isTraceEnabled()) {
+                        log.trace("JWT direct decryption requires a JWK credential, resolved credential "
+                                + "'{}' was not", CredentialConversionUtil.resolveKid(cred));
+                    }
+                    // Require a JWKCredential
+                    continue;
+                }
                 try {
                     validateKeyManagmentAlgorithm(encryptedObject, cred);
-                    validateContentEncryptionAlgorithm(encryptedObject, cred);
-                    final JWEDecrypter decrypter = new DirectDecrypter(cred.getSecretKey());
+                    final EncryptionMethod enc = validateContentEncryptionAlgorithm(encryptedObject, cred);
+                    // Symmetric key to use is derived from shared symmetric key e.g. client_secret
+                    final JWKCredential derivedKey = 
+                            JWKCredentialSupport.deriveSymmetricKeyForAlgAndEnc((JWKCredential)cred, 
+                                    JWEAlgorithm.DIR, enc);
+                    final JWEDecrypter decrypter = new DirectDecrypter(derivedKey.getSecretKey());
                     encryptedObject.decrypt(decrypter);
                     return;
                 } catch (final JOSEException | DecryptionException e) {
@@ -476,16 +505,19 @@ public class JWTDecrypter {
     }
     
     /**
-     * Validates the 'alg' algorithm in the header matches the algorithm specified for the credential, 
-     * validates against the include and exclude algorithm URI lists, and the credential contains the
-     * correct key type.
+     * If the credential contains algorithm 'alg' information, validate the 'alg' algorithm in the JWT JOSE
+     * header matches the algorithm specified for the credential. Then, validates the algorithm in the JWT 
+     * JOSE Header against the include and exclude algorithm URI lists. Finally, validates the credential 
+     * contains the correct key type.
      * 
      * @param encryptedObject the JWE
      * @param cred the credential to validate against the 'alg' header
      * 
+     * @return the validated algorithm to use
+     * 
      * @throws DecryptionException if there is an algorithm mismatch.
      */
-    private void validateKeyManagmentAlgorithm(
+    private JWEAlgorithm validateKeyManagmentAlgorithm(
             @Nonnull final EncryptedJWT encryptedObject, @Nonnull final Credential cred) throws DecryptionException {
         
         if (encryptedObject.getHeader() == null) {
@@ -495,9 +527,9 @@ public class JWTDecrypter {
         final JWEAlgorithm headerAlg = encryptedObject.getHeader().getAlgorithm();
         validateAlgorithmURI(headerAlg.getName());
 
-        if (cred instanceof JWKCredential) {
+        if (cred instanceof JWKCredential && ((JWKCredential)cred).getAlgorithm() != null) {
             final JWKCredential jwkCred = (JWKCredential)cred;
-            if (jwkCred.getAlgorithm() == null || !headerAlg.equals(jwkCred.getAlgorithm())) {
+            if (!headerAlg.equals(jwkCred.getAlgorithm())) {
                 throw new DecryptionException("Credential algorithm '"+jwkCred.getAlgorithm()+"' "
                         + "was not a match for the "
                         + "algorithm '"+encryptedObject.getHeader().getAlgorithm()+"'");
@@ -520,19 +552,23 @@ public class JWTDecrypter {
         }
         
         //All fine    
+        return headerAlg;
     }
     
     
     /**
      * Validates the 'enc' algorithm in the header matches the encryption algorithm specified for the credential, 
-     * and validates against the include and exclude algorithm URI lists.
+     * and validates against the include and exclude algorithm URI lists. If valid, the encryption algorithm from
+     * the header is returned. If not valid, an {@link DecryptionException} is thrown.
      * 
      * @param encryptedObject the JWE
      * @param cred the credential to validate the 'enc' header
      * 
+     * @return the content encryption algorithm from the JOSE header.
+     * 
      * @throws DecryptionException if there is an algorithm mismatch.
      */
-    private void validateContentEncryptionAlgorithm(
+    @Nonnull private EncryptionMethod validateContentEncryptionAlgorithm(
             @Nonnull final EncryptedJWT encryptedObject, @Nonnull final Credential cred) throws DecryptionException {
         
         if (encryptedObject.getHeader() == null) {
@@ -558,8 +594,9 @@ public class JWTDecrypter {
             throw new DecryptionException("JOSE Header 'enc' algorithm "
                     +jcaKeyAlgorithm+" does not match credential algorithm "+cred.getSecretKey().getAlgorithm());
         }
-       
+        
         // Otherwise, all fine.
+        return enc;
     }
     
     /**

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


More information about the commits mailing list