com.nimbusds.jose.crypto.AAD#compute(com.nimbusds.jose.JWEHeader) produces unpredictable output

Issue #183 invalid
Anton Baranoff created an issue

Folks,

Thanks for the great tool. I use JWT 4.12 and I do HMAC over AAD#compute to insure message integrity. However it doesn't work as I expect it to work. So I need your expertise. I found out that com.nimbusds.jose.crypto.AAD#compute(com.nimbusds.jose.JWEHeader) produces unpredictable output

    /**
     * Computes the Additional Authenticated Data (AAD) for the specified
     * JWE header.
     *
     * @param jweHeader The JWE header. Must not be {@code null}.
     *
     * @return The AAD.
     */
    public static byte[] compute(final JWEHeader jweHeader) {

        return compute(jweHeader.toBase64URL());
    }

Somewhere in between the following call takes place

jweHeader.toJSONObject().toString();

toJSONObject produces new JSONObject(...). JSONObject extends HashMap and order of json fields is not guaranteed what leads to unpredictable AAD#compute. Having different AAD for the same header is quite annoying and breaks message validation.

Is my finding correct? Could you please fix the order or help me to find a workaround?

cheers

Comments (6)

  1. Anton Baranoff reporter

    For now I implemented a fixed order ADD

    public class FixedOrderAAD {
    
        public static byte[] compute(final JWEHeader jweHeader) {
            String json = toFixedOrderJSON(jweHeader.toJSONObject());
            Base64URL url = Base64URL.encode(json);
            return AAD.compute(url);
        }
    
        protected static String toFixedOrderJSON(final JSONObject jsonObject) {
            TreeMap treeMap = fixOrder(jsonObject);
            return toJSONString(treeMap);
        }
    
        protected static TreeMap fixOrder(Map<String, Object> map) {
            TreeMap<String, Object> result = new TreeMap<String, Object>();
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                if (entry.getValue() instanceof Map) {
                    result.put(entry.getKey(), fixOrder((Map<String, Object>) entry.getValue()));
                } else {
                    result.put(entry.getKey(), entry.getValue());
                }
            }
            return result;
        }
    }
    
  2. Connect2id OSS

    Hi @ahtokca,

    JOSE was designed to not rely on JSON canonicalisation, i.e. requiring the members of a JSON object (be it header or JWT payload) to be in some predictive order. Instead of that all crypto operations are done on base64url representations. So the AAD is computed on the base64url of the JSON object, regardless of how it is serialised.

    During encryption the JSON object is just serialised (can be any order), then encoded to base64url (to input into AAD computer, etc)

    During decryption the AAD is computed over the base64url directly, i.e. the header is not parsed, then re-serialised: https://bitbucket.org/connect2id/nimbus-jose-jwt/src/a0dadb95881e4e0c66bf1cbb1fca641654762625/src/main/java/com/nimbusds/jose/Header.java?at=master&fileviewer=file-view-default#Header.java-298

    Do you have problems decrypting JOSE objects output by the Nimbus lib in some other library that relies on predictive order?

  3. Anton Baranoff reporter

    Hi Connect2id,

    After you comment walked through the code again and find out that we created com.nimbusds.jose.JWEHeader#JWEHeader(com.nimbusds.jose.JWEAlgorithm, com.nimbusds.jose.EncryptionMethod) twice. HMACing the first one and sending the second. Due to HashMap nature of the JWE header the order was different for the second instance. Thanks for you rapid answer.

  4. Log in to comment