[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