[java-oidc-common] branch main updated: JCOMOIDC-6 Move common crypto/security code from the OIDC plugin

Henri Mikkonen henri.mikkonen at iki.fi
Fri Dec 18 12:37:40 UTC 2020


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=8322a67c802dc03f9e67caddac49d9568844e00f

The following commit(s) were added to refs/heads/main by this push:
       new  8322a67   JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
8322a67 is described below

commit 8322a67c802dc03f9e67caddac49d9568844e00f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Dec 18 14:35:43 2020 +0200

    JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
    
    https://issues.shibboleth.net/jira/browse/JCOMOIDC-6
    
    Imported JWTSignatureValidationUtil and its dependencies from java-idp-oidc.
    Also added unit tests.
---
 .../security/impl/JWTSignatureValidationUtil.java  | 133 +++++++++++++
 .../security/impl/OIDCDecryptionParameters.java    |  40 ++++
 .../impl/OIDCSignatureValidationParameters.java    |  48 +++++
 .../impl/JWTSignatureValidationUtilTest.java       | 207 +++++++++++++++++++++
 4 files changed, 428 insertions(+)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTSignatureValidationUtil.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTSignatureValidationUtil.java
new file mode 100644
index 0000000..0f1d4a9
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWTSignatureValidationUtil.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Iterator;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.ECDSAVerifier;
+import com.nimbusds.jose.crypto.MACVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.JWKCredential;
+
+/**
+ * Generic utility class for helping JWT signature validation.
+ */
+public final class JWTSignatureValidationUtil {
+
+    /** Class logger. */
+    @Nonnull private static Logger log = LoggerFactory.getLogger(JWTSignatureValidationUtil.class);
+
+    /** Private constructor. */
+    private JWTSignatureValidationUtil() {
+
+    }
+
+    /**
+     * Validates the signature of the given JWT using the given security parameters context. If the validation fails for
+     * any reason, including insufficient prequisities in the context, an event identifier is returned. Successful
+     * validation produces null result.
+     * 
+     * @param secParamCtx The {@link SecurityParametersContext} to use for signature validation.
+     * @param signedJwt The signed JWT to be validated.
+     * @param invalidJwtEventId The event identifier describing the invalid JWT.
+     * @return an event ID
+     */
+    public static String validateSignature(final SecurityParametersContext secParamCtx, final SignedJWT signedJwt,
+            final String invalidJwtEventId) {
+        if (secParamCtx == null) {
+            log.error("No security parameters context is available");
+            return EventIds.INVALID_SEC_CFG;
+        }
+        if (secParamCtx.getSignatureSigningParameters() == null
+                || !(secParamCtx.getSignatureSigningParameters() instanceof OIDCSignatureValidationParameters)) {
+            log.error("No signature validation credentials available");
+            return EventIds.INVALID_SEC_CFG;
+        }
+        final OIDCSignatureValidationParameters signatureValidationParameters =
+                (OIDCSignatureValidationParameters) secParamCtx.getSignatureSigningParameters();
+        if (signatureValidationParameters.getValidationCredentials().isEmpty()) {
+            log.error("Unable to find any keys to validate given JWT signature");
+            return EventIds.INVALID_SEC_CFG;            
+        }
+        final Algorithm algorithm = signedJwt.getHeader().getAlgorithm();
+        final Iterator<?> it = signatureValidationParameters.getValidationCredentials().iterator();
+        while (it.hasNext()) {
+            final JWKCredential credential = (JWKCredential) it.next();
+            if (!algorithm.equals(credential.getAlgorithm())) {
+                log.debug("Credential alg {} not matching jwt header alg {}", credential.getAlgorithm().getName(),
+                        algorithm.getName());
+            } else {
+                try {
+                    final JWSVerifier verifier = initializeVerifier(algorithm, credential);
+                    if (verifier == null) {
+                        log.error("No verifier for given JWT for alg {}", algorithm.getName());
+                        return EventIds.INVALID_SEC_CFG;
+                    }
+                    if (signedJwt.verify(verifier)) {
+                        log.debug("JWT {} verified using algorithm {} and key {}", signedJwt.serialize(),
+                                algorithm.getName(), credential.getKid());
+                        return null;
+                    }
+                    log.debug("Unable to validate given JWT with credential, picking next key");
+                } catch (final JOSEException e) {
+                    log.warn("Exception catched when validating given JWT with credential {}", credential.getKid(), e);
+                }
+            }
+        }
+        log.error("Unable to validate given JWT with any of the credentials");
+        return invalidJwtEventId;
+    }
+    
+    /**
+     * Initializes a {@link JWSVerifier} for the given algorithm, using the provided {@link Credential}.
+     * @param algorithm The algorithm used for deciding the verifier.
+     * @param credential The credential to be used for the verifier.
+     * @return A corresponding verifier, or null if no supported found.
+     * @throws JOSEException If the credential doesn't meet the verifier requirements.
+     */
+    private static JWSVerifier initializeVerifier(final Algorithm algorithm, final Credential credential)
+            throws JOSEException {
+        if (JWSAlgorithm.Family.HMAC_SHA.contains(algorithm)) {
+            return new MACVerifier(credential.getSecretKey());
+        }
+        if (JWSAlgorithm.Family.RSA.contains(algorithm)) {
+            return new RSASSAVerifier((RSAPublicKey) credential.getPublicKey());
+        }
+        if (JWSAlgorithm.Family.EC.contains(algorithm)) {
+            return new ECDSAVerifier((ECPublicKey) credential.getPublicKey());
+        }
+        return null;
+    }
+
+}
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/OIDCDecryptionParameters.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/OIDCDecryptionParameters.java
new file mode 100644
index 0000000..5654c7a
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/OIDCDecryptionParameters.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.util.ArrayList;
+import java.util.List;
+
+import org.opensaml.security.credential.Credential;
+import org.opensaml.xmlsec.EncryptionParameters;
+
+/** OIDC Decryption Parameters. Steals a bit EncryptionParameters as extending it for decryption purposes. */
+public class OIDCDecryptionParameters extends EncryptionParameters {
+
+    /** The list of decryption credentials. */
+    private List<Credential> keyTransportDecryptionCredentials = new ArrayList<Credential>();
+
+    /**
+     * Get the list of decryption credentials.
+     * 
+     * @return he list of decryption credentials
+     */
+    public List<Credential> getKeyTransportDecryptionCredentials() {
+        return keyTransportDecryptionCredentials;
+    }
+}
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/OIDCSignatureValidationParameters.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/OIDCSignatureValidationParameters.java
new file mode 100644
index 0000000..6d88ae6
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/OIDCSignatureValidationParameters.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.xmlsec.SignatureSigningParameters;
+
+import net.shibboleth.oidc.security.credential.JWKCredential;
+
+/**
+ * OIDC Signature Validation Parameters. Steals a bit SignatureSigningParameters as extending it also for validation
+ * purposes.
+ */
+public class OIDCSignatureValidationParameters extends SignatureSigningParameters {
+
+    /** The list of validation credentials. */
+    @Nonnull
+    private final List<JWKCredential> validationCredentials = new ArrayList<JWKCredential>();
+
+    /**
+     * Get the list of validation credentials.
+     * 
+     * @return the list of validation credentials
+     */
+    @Nonnull
+    public List<JWKCredential> getValidationCredentials() {
+        return validationCredentials;
+    }
+}
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/JWTSignatureValidationUtilTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/JWTSignatureValidationUtilTest.java
new file mode 100644
index 0000000..9a14434
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/JWTSignatureValidationUtilTest.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.interfaces.ECPrivateKey;
+import java.text.ParseException;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.xmlsec.SignatureSigningParameters;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+
+/**
+ * Unit tests for {@link JWTSignatureValidationUtil}.
+ */
+public class JWTSignatureValidationUtilTest {
+    
+    final String invalidJwtEventId = "invalid_jwt";
+
+    BasicJWKCredential rsaCredential;
+    
+    BasicJWKCredential ecCredential;
+    
+    BasicJWKCredential sharedCredential;
+    
+    @BeforeClass
+    public void init() throws NoSuchAlgorithmException {
+        rsaCredential = createKeyPairCredential("RSA", "RS256", "mockRSAKey", 2048);
+        ecCredential = createKeyPairCredential("EC", "ES256", "mockECKey", 256);
+        sharedCredential = createSharedSecretCredential("HS256", "mockSharedSecret");
+    }
+    
+    @Test
+    public void validateSignature_shouldReturnInvalidSecCfgWhenNoSecCtx() {
+        Assert.assertEquals(JWTSignatureValidationUtil.validateSignature(null, null, invalidJwtEventId),
+                EventIds.INVALID_SEC_CFG);
+    }
+
+    @Test
+    public void validateSignature_shouldReturnInvalidSecCfgWhenNoSigningParameters() {
+        SecurityParametersContext secCtx = new SecurityParametersContext();
+        Assert.assertEquals(JWTSignatureValidationUtil.validateSignature(secCtx, null, invalidJwtEventId),
+                EventIds.INVALID_SEC_CFG);
+    }
+
+    @Test
+    public void validateSignature_shouldReturnInvalidSecCfgWhenWrongSigningParameters() {
+        SecurityParametersContext secCtx = new SecurityParametersContext();
+        secCtx.setSignatureSigningParameters(new SignatureSigningParameters());
+        Assert.assertEquals(JWTSignatureValidationUtil.validateSignature(secCtx, null, invalidJwtEventId),
+                EventIds.INVALID_SEC_CFG);
+    }
+
+    @Test
+    public void validateSignature_shouldReturnNullWhenValidRSASignature()
+            throws JOSEException, ParseException, NoSuchAlgorithmException {
+        SecurityParametersContext secCtx = initSecurityParamsContext("RS256", rsaCredential);
+        Assert.assertNull(JWTSignatureValidationUtil.validateSignature(secCtx, signRSA(rsaCredential.getPrivateKey()),
+                invalidJwtEventId));
+    }
+    
+    @Test
+    public void validateSignature_shouldReturnEventIdWhenInvalidRSASignature()
+            throws NoSuchAlgorithmException, JOSEException, ParseException {
+        final BasicJWKCredential anotherRsaCredential = createKeyPairCredential("RSA", "RS256", "mockRSAKey2", 2048);
+        final SecurityParametersContext secCtx = initSecurityParamsContext("RS256", rsaCredential);
+        Assert.assertEquals(JWTSignatureValidationUtil.validateSignature(secCtx,
+                signRSA(anotherRsaCredential.getPrivateKey()), invalidJwtEventId), invalidJwtEventId);        
+    }
+    
+    @Test
+    public void validateSignature_shouldReturnNullWhenValidECSignature()
+            throws JOSEException, ParseException, NoSuchAlgorithmException {
+        SecurityParametersContext secCtx = initSecurityParamsContext("ES256", ecCredential);
+        Assert.assertNull(JWTSignatureValidationUtil.validateSignature(secCtx,
+                signEC((ECPrivateKey) ecCredential.getPrivateKey()), invalidJwtEventId));
+    }
+
+    @Test
+    public void validateSignature_shouldReturnEventIdWhenInvalidECSignature()
+            throws JOSEException, ParseException, NoSuchAlgorithmException {
+        final BasicJWKCredential anotherEcCredential = createKeyPairCredential("EC", "ES256", "mockECKey2", 256);
+        SecurityParametersContext secCtx = initSecurityParamsContext("ES256", ecCredential);
+        Assert.assertEquals(JWTSignatureValidationUtil.validateSignature(secCtx,
+                signEC((ECPrivateKey) anotherEcCredential.getPrivateKey()), invalidJwtEventId), invalidJwtEventId);        
+    }
+
+    @Test
+    public void validateSignature_shouldReturnNullWhenValidMACSignature() throws JOSEException, ParseException {
+        SecurityParametersContext secCtx = initSecurityParamsContext("HS256", sharedCredential);
+        Assert.assertNull(JWTSignatureValidationUtil.validateSignature(secCtx, 
+                signMAC(sharedCredential.getSecretKey()), invalidJwtEventId));
+    }
+
+    @Test
+    public void validateSignature_shouldReturnEventIdWhenInvalidMACSignature() throws JOSEException, ParseException {
+        SecurityParametersContext secCtx = initSecurityParamsContext("HS256", sharedCredential);
+        Assert.assertEquals(JWTSignatureValidationUtil.validateSignature(secCtx, 
+                signMAC(generateSecretKey()), invalidJwtEventId), invalidJwtEventId);
+        
+    }
+
+    protected SignedJWT signRSA(final PrivateKey privateKey) throws JOSEException, ParseException {
+        final JWSSigner signer = new RSASSASigner(privateKey);
+        final JWSObject jwsObject = new JWSObject(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("mockId").build(),
+                new Payload("RSA payload"));
+
+        jwsObject.sign(signer);
+        return SignedJWT.parse(jwsObject.serialize());
+    }
+
+    protected SignedJWT signEC(final ECPrivateKey privateKey) throws JOSEException, ParseException {
+        final JWSSigner signer = new ECDSASigner(privateKey);
+        final JWSObject jwsObject = new JWSObject(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("mockId").build(),
+                new Payload("RSA payload"));
+
+        jwsObject.sign(signer);
+        return SignedJWT.parse(jwsObject.serialize());
+    }
+
+    protected BasicJWKCredential createKeyPairCredential(final String keyPairAlgorithm, final String jwsAlgorithm,
+            final String kid, final int keysize) throws NoSuchAlgorithmException {   
+        final KeyPairGenerator generator = KeyPairGenerator.getInstance(keyPairAlgorithm);
+        generator.initialize(keysize);
+        final KeyPair keyPair = generator.generateKeyPair();
+        final BasicJWKCredential credential = new BasicJWKCredential();
+        credential.setAlgorithm(JWSAlgorithm.parse(jwsAlgorithm));
+        credential.setPublicKey(keyPair.getPublic());
+        credential.setPrivateKey(keyPair.getPrivate());
+        credential.setKid(kid);
+
+        return credential;
+    }
+    
+    protected BasicJWKCredential createSharedSecretCredential(final String jwsAlgorithm, final String kid) {
+        final BasicJWKCredential credential = new BasicJWKCredential();
+        credential.setAlgorithm(JWSAlgorithm.parse(jwsAlgorithm));
+        credential.setSecretKey(generateSecretKey());
+        credential.setKid(kid);
+
+        return credential;        
+    }
+    
+    protected SecretKey generateSecretKey() {
+        final SecureRandom random = new SecureRandom();
+        final byte[] sharedSecret = new byte[32];
+        random.nextBytes(sharedSecret);
+        return new SecretKeySpec(sharedSecret, "AES");
+    }
+    
+    protected SignedJWT signMAC(final SecretKey secretKey) throws JOSEException, ParseException {
+        final JWSSigner signer = new MACSigner(secretKey.getEncoded());
+        final JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), new Payload("MAC payload"));
+        jwsObject.sign(signer);
+        return SignedJWT.parse(jwsObject.serialize());
+    }
+    
+    protected SecurityParametersContext initSecurityParamsContext(final String algorithm, 
+            final BasicJWKCredential... credentials) {
+        SecurityParametersContext secCtx = new SecurityParametersContext();
+        OIDCSignatureValidationParameters params = new OIDCSignatureValidationParameters();
+        for (final BasicJWKCredential credential : credentials) {
+            params.getValidationCredentials().add(credential);
+        }
+        params.setSignatureAlgorithm(algorithm);
+        secCtx.setSignatureSigningParameters(params);
+        return secCtx;
+    }
+}

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


More information about the commits mailing list