openssl / openssl

TLS/SSL and crypto library

Home Page:https://www.openssl.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

KBKDF (SP 800 108 KDF) wrong result for non-empty salt in feedback mode

guidovranken opened this issue · comments

#include <openssl/kdf.h>
#include <openssl/params.h>
#include <openssl/core_names.h>
#include <string.h>

static void error(const char* e) {
    printf("%s\n", e);
    abort();
}

int main(void)
{
    EVP_KDF *kdf;
    EVP_KDF_CTX *kctx;
    unsigned char out[32];
    OSSL_PARAM params[7], *p = params;

    const unsigned char salt[] = {0x00};
    //const unsigned char secret[] = {0x00};
    const unsigned char secret[] = {};
    const unsigned char label[] = {0x00};

    kdf = EVP_KDF_fetch(NULL, "KBKDF", NULL);
    kctx = EVP_KDF_new_ctx(kdf);
    EVP_KDF_free(kdf);

    *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST,
            "SHA384", 0);
    *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_MAC,
            "HMAC", 0);
    *p++ = OSSL_PARAM_construct_utf8_string(
            OSSL_KDF_PARAM_MODE, "COUNTER", 0);
    *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY,
            secret, sizeof(secret));
    *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT,
            label, sizeof(label));
    *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO,
            salt, sizeof(salt));
    *p = OSSL_PARAM_construct_end();
    if (EVP_KDF_set_ctx_params(kctx, params) <= 0)
        error("EVP_KDF_set_ctx_params");
    else if (EVP_KDF_derive(kctx, out, sizeof(out)) <= 0)
        error("EVP_KDF_derive");
    for (size_t i = 0; i < sizeof(out); i++) {
        printf("%02X ", out[i]);
    }
    printf("\n");

    EVP_KDF_free_ctx(kctx);
    return 0;
}
commented

What version are you using? I suspect that #11920 would fix this.

Tested on commit f6f159e, so one commit behind on master. So that PR didn't fix it.

Additionally, KBKDF seems to produce the incorrect output in feedback mode if OSSL_KDF_PARAM_INFO is not empty. Either that parameter is ignored, or incorrectly processed. This was discovered through differential fuzzing against Botan.

commented

@guidovranken it was remarked on #12826 that this issue may not be a problem anymore; could you please confirm whether or not this issue should be closed?

Tested again, looks fixed now.

Reopening because while the null pointer is fixed, the wrong result for non-empty salt in feedback mode is not.

Example:

Operation:
operation name: KDF_SP_800_108
hmac/cmac: HMAC
digest: MD5
secret: {0x00, 0x00} (2 bytes)
salt: {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} (43 bytes)
label: {}
mode: 1
keySize: 13

Module Botan result:

{0x83, 0x8c, 0xa7, 0x48, 0x3d, 0x3e, 0xce, 0x81, 0xda, 0x24, 0x5a, 0xd0, 0x4a} (13 bytes)

Module OpenSSL result:

{0xeb, 0x21, 0x34, 0xc5, 0x61, 0xcc, 0x42, 0xfc, 0xa5, 0x98, 0x70, 0x1c, 0x05} (13 bytes)
commented

Are botan doing the correct thing?? (I am not sure they are)


KDF_FEEDBACK(.... ,salt, salt_len)
{
   iv_len = (salt_len >= prf_len ? prf_len : 0);
 
  memcpy(prev, salt, iv_len);
  memcpy(ctx, salt + iv_len, salt_len - iv_len);

  for(...) {
       ...
      prev = prf(prev || counter || label || delim || ctx || L);
      ...
}

So you can see that they potentially feed any leftover part of the iv in via the ctx..
This doesnt exactly look like SP800-108 which just says that the context is an input?? Maybe I am missing something.

@guidovranken I think this is coming up because neither Botan nor OpenSSL's KDF interface (nor Cryptofuzz's itself, it seems) exactly maps onto what SP800-108 uses.

  • Botan uses secret, label, and salt.
  • OpenSSL uses secret, salt, info, and seed
  • SP800-108 Feedback uses secret, label, context, and IV.

Secret we all agree on. SP800-108's label is Botan's label or OpenSSL's salt. OpenSSL maps info to context, and seed to IV. So OpenSSL is able to set all 4 variables independently. However Botan KDF interface is missing an argument that can cover IV, mostly because nothing else uses this. So what Botan's Feedback KDF does is the following logic: if the salt length is less than the PRF, use the entire thing as the context and leave IV empty. Otherwise take the first PRF-length block as the IV and leave the rest as the context.

Looking at it now I'm thinking this was a mistake on Botan's part - we should have just left IV empty in all cases and mapped salt directly onto context, effectively what OpenSSL does except ignoring the seed/IV argument. (I didn't write this KDF logic but I did approve it so I guess it's my problem now)

In the test case you produced above, Botan is interpreting the long salt as an IV/context pair, while OpenSSL leaves IV empty and uses the whole thing as context. I don't think there is a bug on either side here, exactly, just a different encoding mechanism which causes things to be incompatible :/ The simplest thing to do is skip Botan's Feedback KDF when the salt is longer than the PRF output, since that is what will trigger the IV splitting logic. Alternately, add support for this "Seed"/"IV" notion into cryptofuzz (this is afaik completely unique to Feedback KDF so maybe not worth bothering). For OpenSSL, set it directly. For Botan, prefix it to the salt. Then you should see identical results across all inputs.

Thank you for your thorough analysis @randombit !

I will apply your suggestions to Cryptofuzz so this won't come up again.

Closing this issue now.