[java-oidc-common] branch main updated: Add JWS assembly support tools

Phil Smart philip.smart at jisc.ac.uk
Thu Feb 4 15:26:46 UTC 2021


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

philsmart 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=4b2294b0d4f6024ed0095f427def6391663255f4

The following commit(s) were added to refs/heads/main by this push:
       new  4b2294b   Add JWS assembly support tools
4b2294b is described below

commit 4b2294b0d4f6024ed0095f427def6391663255f4
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Feb 4 15:26:43 2021 +0000

    Add JWS assembly support tools
    
    Useful when generating a JWS bypassing some of the security controls
    around key sizes (needed for Duo).
---
 .../oidc/security/impl/JWSAssemblyUtils.java       | 188 +++++++++++++++++++++
 .../oidc/security/impl/JWSAssemblyUtilsTest.java   | 173 +++++++++++++++++++
 2 files changed, 361 insertions(+)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWSAssemblyUtils.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWSAssemblyUtils.java
new file mode 100644
index 0000000..7b06061
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/impl/JWSAssemblyUtils.java
@@ -0,0 +1,188 @@
+/*
+ * 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.nio.charset.StandardCharsets;
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.impl.AlgorithmSupportMessage;
+import com.nimbusds.jose.crypto.impl.HMAC;
+import com.nimbusds.jose.crypto.impl.MACProvider;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** 
+ * Generic utility class for helping with JWS assembly.
+ */
+public final class JWSAssemblyUtils {
+    
+    /** Private constructor.*/
+    private JWSAssemblyUtils() {
+        
+    }
+    
+    /**
+     * Assemble a HMAC based JSON Web Signature token using the given algorithm, claims, and secret,
+     * and return its serialized form.
+     * 
+     * <p>Note, can be used over the Nimbus {@link MACSigner} on occasions where an algorithm to 
+     * secret key compatibility issues exist e.g. using a HS512 HMAC with too small a secret key</p>
+     * 
+     * @param algorithm the JWA algorithm, **must** be one from the HMAC family.
+     * @param claimsSet the claims that form the payload.
+     * @param secret the pre-shared secret used to construct the HMAC.
+     * 
+     * @return a fully assembled JWS using the JSON compact serialisation.
+     * 
+     * @throws EncodingException On error during encoding.
+     * @throws JOSEException If the algorithm is not supported.
+     * @throws ParseException If an error occurs during serialisation.
+     */
+    @Nonnull public static String assembleMacJwsAsString(@Nonnull final JWSAlgorithm algorithm, 
+            @Nonnull final JWTClaimsSet claimsSet, @Nonnull final byte[] secret) throws 
+                    EncodingException, JOSEException, ParseException {
+
+        return assembleMacJws(algorithm,claimsSet,secret).serialize();
+        
+    }
+    
+    /**
+    * Assemble a HMAC based JSON Web Signature token (JWS) using the given algorithm, claims, and secret.
+    * 
+    * <p>Note, can be used over the Nimbus {@link MACSigner} on occasions where an algorithm to 
+    * secret key compatibility issues exist e.g. using a HS512 HMAC with too small a secret key</p>
+    * 
+    * @param algorithm the JWA algorithm, **must** be one from the HMAC family.
+    * @param claimsSet the claims that form the payload.
+    * @param secret the pre-shared secret used to construct the HMAC.
+    * 
+    * @return a fully assembled JWS.
+    * 
+    * @throws EncodingException On error during encoding.
+    * @throws JOSEException If the algorithm is not supported.
+    * @throws ParseException If an error occurs during serialisation.
+    */
+   @Nonnull public static SignedJWT assembleMacJws(@Nonnull final JWSAlgorithm algorithm, 
+           @Nonnull final JWTClaimsSet claimsSet, @Nonnull final byte[] secret) throws 
+                   EncodingException, JOSEException, ParseException {
+       
+       Constraint.isNotNull(algorithm, "Algorithm can not be null");
+       Constraint.isNotNull(claimsSet, "JWT claims can not be null");
+       Constraint.isNotNull(secret, "Secret can not be null");
+       
+       final JWSHeader header = new JWSHeader.Builder(algorithm)
+               .type(JOSEObjectType.JWT)
+               .build();        
+       final Payload payload = new Payload(claimsSet.toJSONObject());
+       
+       final String signingInput = composeSigningInput(header,payload);
+     
+       /*
+        * A null JCA provider is supplied, as a result the preferred provider that supports the specified
+        *  algorithm will be used.
+        */
+       final byte[] hmac = HMAC.compute(getJCAAlgorithmName(algorithm), secret, 
+               signingInput.getBytes(StandardCharsets.UTF_8), null);
+       
+       final SignedJWT signedJwt = new SignedJWT(header.toBase64URL(), payload.toBase64URL(), 
+               Base64URL.encode(hmac));
+
+       return signedJwt;
+       
+   }
+    
+    
+    /**
+     * Convert the String secret into its byte representation assuming a UTF-8 encoding.
+     * 
+     * @param secret the secret as a UTF-8 encoding string, must not be {@code null}.
+     * 
+     * @return the UTF-8 byte representation of the secret.
+     */
+    @Nonnull public static byte[] getSecretBytes(@Nonnull final String secret) {
+        return secret.getBytes(StandardCharsets.UTF_8);
+    }
+    
+    /**
+     * Compose the message that is to be signed. 
+     * 
+     * <p>From RFC7515: {@literal ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||
+      BASE64URL(JWS Payload)}
+     * 
+     * @param header the header component of the message to be signed.
+     * @param payload the payload component of the message to be signed.
+     * 
+     * @return the message in its compact/serialised state ready to be signed.
+     * 
+     * @throws EncodingException if there is an error base64 encoding the components.
+     */
+    @Nonnull private static String composeSigningInput(@Nonnull final JWSHeader header,
+            @Nonnull final Payload payload) throws EncodingException {
+        Constraint.isNotNull(header, "JWS Header can not be null");
+        Constraint.isNotNull(payload, "JWT payload can not be null");
+        
+        return header.toBase64URL().toString() + "."+ payload.toBase64URL().toString();
+    }
+    
+    
+    /**
+     * Gets the matching Java Cryptography Architecture (JCA) algorithm 
+     * name for the specified HMAC-based JSON Web Algorithm (JWA).
+     * <p>
+     * This is taken from the Nimbus {@link MACProvider} class.
+     * </p>
+     *
+     * @param alg The JSON Web Algorithm (JWA). Must be supported and not
+     *            {@code null}.
+     *
+     * @return The matching JCA algorithm name.
+     *
+     * @throws JOSEException If the algorithm is not supported.
+     */
+    @Nonnull private static String getJCAAlgorithmName(@Nonnull final JWSAlgorithm alg)
+        throws JOSEException {
+        Constraint.isNotNull(alg, "Algorithm can not be null");
+
+        if (alg.equals(JWSAlgorithm.HS256)) {
+            return "HMACSHA256";
+        } else if (alg.equals(JWSAlgorithm.HS384)) {
+            return "HMACSHA384";
+        } else if (alg.equals(JWSAlgorithm.HS512)) {
+            return "HMACSHA512";
+        } else {
+            throw new JOSEException(AlgorithmSupportMessage.unsupportedJWSAlgorithm(
+                alg,
+                MACProvider.SUPPORTED_ALGORITHMS));
+        }
+    }
+    
+
+}
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/JWSAssemblyUtilsTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/JWSAssemblyUtilsTest.java
new file mode 100644
index 0000000..2b13a59
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/impl/JWSAssemblyUtilsTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
+
+import java.nio.charset.StandardCharsets;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jose.util.JSONObjectUtils;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+
+/** Test for {@link JWSAssemblyUtils}.*/
+public class JWSAssemblyUtilsTest {
+    
+    /** 
+     * A Simple JSON claims payload from RFC7515.
+     */
+    private static final String SIMPLE_CLAIMS_PAYLOAD =
+            "{\"iss\":\"joe\",\n"
+            + "      \"exp\":1300819380,\n"
+            + "      \"http://example.com/is_root\":true}";
+    
+    
+    /**
+     * Test JWS generation and compare the generated HMAC against a newly generated HMAC from the
+     * same set of payload and content bytes. 
+     * 
+     * <p>Note, as there is no process of 'canonicalization' we can not reliable compare a newly generated
+     * version of this JWS using a different JWT generation method - for instance it may not escape special characters,
+     * or may order the claims differently. As a result, we check the {@literal HMAC(Header.Payload)} by taking the
+     * header and payload bytes from the generated JWS and compare to the generated signature component from the same
+     * JWS.</p>
+ 
+     * 
+     * <p>Use HMAC-SHA-256 for compatibility with the RFC.</p>
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    public final void testAssembleMacJws() throws Exception {
+        
+        final JWTClaimsSet set = JWTClaimsSet.parse(SIMPLE_CLAIMS_PAYLOAD);
+        final String secret = "tiny";
+        //HMAC using SHA-256
+        final SignedJWT jws = JWSAssemblyUtils.assembleMacJws(
+                JWSAlgorithm.HS256, set, secret.getBytes(StandardCharsets.UTF_8));
+        final Base64URL sigBase64URL = jws.getSignature();
+        assertNotNull(sigBase64URL);
+        
+        //check it using low level Java APIs to create the same HMAC from the same serialisation
+        //of header.payload
+        final SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
+        final Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(keySpec);
+        //convert into JSON and back to base64URL to be sure (is almost redundant)
+        final String jsonHeader = JSONObjectUtils.toJSONString(jws.getHeader().toJSONObject());
+        final String jsonPayload = JSONObjectUtils.toJSONString(jws.getPayload().toJSONObject());
+           
+        
+        final String signingInput = Base64URL.encode(jsonHeader.getBytes()).toString()+"."+
+                Base64URL.encode(jsonPayload.getBytes());        
+        
+        byte[] computedHmac = mac.doFinal(signingInput.getBytes(StandardCharsets.UTF_8));
+        
+        //of note, assertEquals would suffer from a timing attack if used in something other than tests :-)
+        assertEquals(sigBase64URL.decode(), computedHmac);
+    }
+    
+    /**
+     * 
+     * Test verify the computed JWS HMAC using a HMAC generated with a different key. Probably
+     * little worth in this test, other than to check generation is happening using the stated 
+     * key. 
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    public final void testAssembleMacJwsCheckWithDifferentKey() throws Exception {
+        
+        final JWTClaimsSet set = JWTClaimsSet.parse(SIMPLE_CLAIMS_PAYLOAD);
+        final String secret = "thisisanexamplesecret";
+        final String differentSecret = "thisisadifferentsecret";
+        //HMAC using SHA-256
+        final SignedJWT jws = JWSAssemblyUtils.assembleMacJws(
+                JWSAlgorithm.HS256, set, secret.getBytes(StandardCharsets.UTF_8));
+        final Base64URL sigBase64URL = jws.getSignature();
+        assertNotNull(sigBase64URL);
+        
+        //check it using low level Java APIs to create the same HMAC from the same serialisation
+        //of header.payload but using a different key
+        final SecretKeySpec keySpec = new SecretKeySpec(differentSecret.getBytes(), "HmacSHA256");
+        final Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(keySpec);
+        //convert into JSON and back to base64URL to be sure (is almost redundant)
+        final String jsonHeader = JSONObjectUtils.toJSONString(jws.getHeader().toJSONObject());
+        final String jsonPayload = JSONObjectUtils.toJSONString(jws.getPayload().toJSONObject());
+           
+        
+        final String signingInput = Base64URL.encode(jsonHeader.getBytes()).toString()+"."+
+                Base64URL.encode(jsonPayload.getBytes());        
+        
+        byte[] computedHmac = mac.doFinal(signingInput.getBytes(StandardCharsets.UTF_8));
+        
+        //of note, assertEquals would suffer from a timing attack if used in something other than tests :-)
+        assertNotEquals(sigBase64URL.decode(), computedHmac);
+    }
+    
+    /**
+     * 
+     * Test verify the computed JWS HMAC using a HMAC generated with a different key. Probably
+     * little worth in this test, other than to check generation is happening using the stated 
+     * algorithm. 
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    public final void testAssembleMacJwsCheckWithDifferentAlgorithm() throws Exception {
+        
+        final JWTClaimsSet set = JWTClaimsSet.parse(SIMPLE_CLAIMS_PAYLOAD);
+        final String secret = "thisisanexamplesecret";
+        //HMAC using SHA-256
+        final SignedJWT jws = JWSAssemblyUtils.assembleMacJws(
+                JWSAlgorithm.HS256, set, secret.getBytes(StandardCharsets.UTF_8));
+        final Base64URL sigBase64URL = jws.getSignature();
+        assertNotNull(sigBase64URL);
+        
+        //check it using low level Java APIs to create the same HMAC from the same serialisation
+        //of header.payload but using a different algorithm
+        final SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA512");
+        final Mac mac = Mac.getInstance("HmacSHA512");
+        mac.init(keySpec);
+        //convert into JSON and back to base64URL to be sure (is almost redundant)
+        final String jsonHeader = JSONObjectUtils.toJSONString(jws.getHeader().toJSONObject());
+        final String jsonPayload = JSONObjectUtils.toJSONString(jws.getPayload().toJSONObject());
+           
+        
+        final String signingInput = Base64URL.encode(jsonHeader.getBytes()).toString()+"."+
+                Base64URL.encode(jsonPayload.getBytes());        
+        
+        byte[] computedHmac = mac.doFinal(signingInput.getBytes(StandardCharsets.UTF_8));
+        
+        //of note, assertEquals would suffer from a timing attack if used in something other than tests :-)
+        assertNotEquals(sigBase64URL.decode(), computedHmac);
+    }
+
+}

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


More information about the commits mailing list