[java-oidc-common] 02/02: JCOMOIDC-113 - TrustEngine implementation for token derived credentials

Henri Mikkonen henri.mikkonen at iki.fi
Wed May 8 09:46:05 UTC 2024


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

hjmikkon pushed a commit to branch main
in repository java-oidc-common.

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

commit a9ea3e9885ee833382c1c92f2f77604b4130248f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed May 8 12:43:09 2024 +0300

    JCOMOIDC-113 - TrustEngine implementation for token derived credentials
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-113
    
    New TokenAsymmetricKeyTrustEngine bases trust on token derived asymmetric public key
---
 .../impl/TokenAsymmetricKeyTrustEngine.java        |  91 ++++++++++++
 .../impl/TokenAsymmetricKeyTrustEngineTest.java    | 157 +++++++++++++++++++++
 2 files changed, 248 insertions(+)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/TokenAsymmetricKeyTrustEngine.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/TokenAsymmetricKeyTrustEngine.java
new file mode 100644
index 0000000..bc27ad7
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/TokenAsymmetricKeyTrustEngine.java
@@ -0,0 +1,91 @@
+/*
+ * 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 net.shibboleth.oidc.security.impl;
+
+import java.security.PublicKey;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.trust.TrustEngine;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.AsymmetricJWK;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * A {@link TrustEngine} implementation for Signed JSON Web Tokens. The token derived public key is used as the trust
+ * basis.
+ * 
+ * @since 3.2.0
+ */
+public class TokenAsymmetricKeyTrustEngine extends BaseSignedJWTTrustEngine<Credential> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExplicitKeySignedJWTTrustEngine.class);
+
+    /**
+     * Constructor.
+     * 
+     * @param joseObjectResolver resolver of credentials from JOSEObject headers.
+     */
+    protected TokenAsymmetricKeyTrustEngine(
+            @Nonnull final @ParameterName(name="JOSEObjectResolver") JOSEObjectCredentialResolver joseObjectResolver) {
+        super(joseObjectResolver);
+    }
+
+    @Override
+    /** {@inheritDoc} */
+    protected boolean doValidate(@Nonnull final SignedJWT signedJWT, @Nonnull final CriteriaSet trustBasisCriteria)
+            throws SecurityException {
+        final JWK jwk = signedJWT.getHeader().getJWK();
+        if (jwk != null) {
+            try {
+                if (!(jwk instanceof AsymmetricJWK)) {
+                    log.warn("JWK in JWT is not asymmetric");
+                    return false;
+                }
+                return validate(signedJWT, CredentialConversionUtil.keyToCredential(jwk));
+            } catch (final SecurityException | JOSEException e) {
+                log.warn("Could not convert the incoming JWK into credential", e);
+            }
+        } else {
+            log.warn("No JWK found from the incoming JWT");
+        }
+        return false;
+    }
+
+    @Override
+    /** {@inheritDoc} */
+    protected boolean evaluateTrust(@Nonnull final Credential untrustedCredential,
+            @Nullable final Credential trustBasis) throws SecurityException {
+        final PublicKey publicKey = untrustedCredential.getPublicKey();
+        if (publicKey != null && trustBasis != null) {
+            return publicKey.equals(trustBasis.getPublicKey());
+        }
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/TokenAsymmetricKeyTrustEngineTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/TokenAsymmetricKeyTrustEngineTest.java
new file mode 100644
index 0000000..ab976d3
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/TokenAsymmetricKeyTrustEngineTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 net.shibboleth.oidc.security.impl;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+
+import net.shibboleth.oidc.jwa.support.SignatureConstants;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.SignatureValidationParameters;
+import net.shibboleth.oidc.security.jose.criterion.SignatureValidationParametersCriterion;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/** 
+ * Unit tests for {@link TokenAsymmetricKeyTrustEngine}.
+ */
+ at SuppressWarnings("null")
+public class TokenAsymmetricKeyTrustEngineTest {
+
+    private TokenAsymmetricKeyTrustEngine engine;
+    
+    private static final String CLIENT_SECRET = "Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$";
+    
+    private SignatureValidationParameters params;
+    
+    private CriteriaSet criteria;
+    
+    private ECKey ecKey;
+    
+    @BeforeMethod
+    public void setup() throws JOSEException {
+        
+        //Setup a standard security params context
+        params = new SignatureValidationParameters();
+        params.setSignatureTrustEngine(engine);
+        
+        // Setup standard criterion
+        criteria = new CriteriaSet();
+        criteria.add(new UsageCriterion(UsageType.SIGNING));
+        criteria.add(new SignatureValidationParametersCriterion(params));
+        
+        final JOSEObjectCredentialResolver joseObjectCredResolver = new BasicJOSEObjectCredentialResolver();
+        
+        engine = new TokenAsymmetricKeyTrustEngine(joseObjectCredResolver);
+        ecKey = new ECKeyGenerator(Curve.P_256).keyID("123").generate();
+    }
+
+    @Test
+    public void testInvalid_WithoutInlineECCredential() throws JOSEException, SecurityException {
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createECSignedJWT(ecKey, ecKey.getKeyID(),
+                JWSAlgorithm.ES256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+    
+    @Test
+    public void testInvalid_WithSymmetricKeyCredential() throws JOSEException, SecurityException {
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createMACSignedJWT(CLIENT_SECRET,
+                "mockId", JWSAlgorithm.HS256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+    
+    @Test
+    public void testInvalid_WithSymmetricKeyCredential_JWSAlgorithm_Excluded() throws JOSEException, SecurityException {
+        params.setExcludedAlgorithms(List.of(SignatureConstants.ALGO_ID_SIGNATURE_HS_256));
+        
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createMACSignedJWT(CLIENT_SECRET,
+                "mockId", JWSAlgorithm.HS256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+    
+    @Test
+    public void testValid_WithInlineJWK() throws JOSEException, SecurityException {       
+        
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createECSignedJWTWithInlineJWK(ecKey,
+                ecKey.getKeyID(), JWSAlgorithm.ES256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertTrue(valid);
+    }
+
+    @Test
+    public void testInvalid_WithInlineJWK_JWSAlgorithm_Excluded() throws JOSEException, SecurityException {       
+        params.setExcludedAlgorithms(List.of(SignatureConstants.ALGO_ID_SIGNATURE_ES_256));
+        
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createECSignedJWTWithInlineJWK(ecKey,
+                ecKey.getKeyID(), JWSAlgorithm.ES256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+
+    @Test
+    public void testInvalid_WithInlineJWK_JWSAlgorithm_notIncluded() throws JOSEException, SecurityException {       
+        params.setIncludedAlgorithms(List.of(SignatureConstants.ALGO_ID_SIGNATURE_RS_256));
+        
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createECSignedJWTWithInlineJWK(ecKey,
+                ecKey.getKeyID(), JWSAlgorithm.ES256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+
+    @Test
+    public void testInvalid_InlineJWKWrongKid() throws JOSEException, SecurityException {
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createECSignedJWTWithInlineJWK(ecKey,
+                "WRONG-KID", JWSAlgorithm.ES256, 
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+    
+    @Test
+    public void testInvalid_InlineJWKInvalidSignature() throws JOSEException, SecurityException {
+        
+        final var valid = engine.validate(ExplicitKeySignedJWTTrustEngineTest.createECSignedJWTWithDifferentInlineJWK(
+                ecKey, ecKey.getKeyID(),JWSAlgorithm.ES256,
+                // A new signing key, different to the one described in the header
+                new ECKeyGenerator(Curve.P_256).keyID("new").generate(),
+                "https://op.example.com/", "https://rp.example.com"),
+                criteria);
+        assertFalse(valid);
+    }
+
+}
\ No newline at end of file

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


More information about the commits mailing list