espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.23k stars 7.18k forks source link

[ESP Digital Signature] esp_ds_sign() does not support Virtual Fuses ?❗❗ ESP_ERR_HW_CRYPTO_DS_HMAC_FAIL (IDFGH-8575) #10025

Closed chipweinberger closed 1 year ago

chipweinberger commented 1 year ago

Is there any way to test digital signature code without actually burning fuses?

I'm trying to get ESP Digital Signature working, but esp_ds_sign() returns ESP_ERR_HW_CRYPTO_DS_HMAC_FAIL, likely due to ets_hmac_calculate_downstream() not supporting virtual fuses.

Is there anything like esp_test_ds_sign(uint8_t* hmacKey256, ...) that lets you pass in the hmac key without actually burning the fuses? Even if it was just implemented in software..

The code is quite complicated (reproduced below).

WARNING: this code does not work!

generating Y,M,Rb,MPrime & burning:

# python's cryptography does not 
# support signing messages without padding, 
# so we must do it ourself!
def rsaSign(messageHash, d, n):
    signature = pow(messageHash,d,n)
    return signature

def rsaVerify(signature, e, n):
    messageHash = pow(signature,e,n)
    return messageHash

def burnPrivateKey(st):
    # pem files
    p = getPaths(st)

    print("----------------------------------------------------------------------------")
    print("----------------------------------------------------------------------------")
    print("---------------     [Auth Esp] Burn Private Key      -----------------------")
    print("----------------------------------------------------------------------------")
    print("----------------------------------------------------------------------------")

    pemFile = open(p.devPrivate_Pem, "rb") 
    pemData = pemFile.read()
    pemFile.close()

    private_key = serialization.load_pem_private_key(pemData, password=None, backend=default_backend())

    if not isinstance(private_key, rsa.RSAPrivateKey):
        bigPrint.print_fail()
        print("expected PEM to be RSAPrivateKey")
        exit(2)

    print("signing...\n")

    priv_numbers = private_key.private_numbers()
    pub_numbers = private_key.public_key().public_numbers()

    Y = priv_numbers.d # RSA Exponent
    M = pub_numbers.n  # RSA modulus

    # this cryptic code came from esspressif
    # /esp_secure_cert_mgr/blob/main/tools/esp_secure_cert/configure_ds.py
    # The ESP RSA accelerator is based on Montgomery multiplication.
    # Therefore, aside from the X, Y , and M arguments, 
    # two additional ones are needed — rinv and mprime
    # Which we need to calculate in advance
    key_size = private_key.key_size

    # calculate rinv == Rb
    rr = 1 << (key_size * 2)
    rinv = rr % pub_numbers.n # RSA r inverse operand

    # calculate MPrime
    mprime = - rsa._modinv(M, 1 << 32)
    mprime &= 0xFFFFFFFF # RSA M prime operand

    cmdJson = {}
    cmdJson['rsaY'] = hex(Y).replace("0x","")
    cmdJson['rsaM'] = hex(M).replace("0x","")
    cmdJson['rsaRb'] = hex(rinv).replace("0x","")
    cmdJson['rsaMPrime'] = hex(mprime).replace("0x","")

    # Serializing json
    jsonStr = json.dumps(cmdJson, indent=4).replace('\n','').replace(' ','')

    jResp = jflashConsole.invoke_json_cmd(st,"/api/auth/esp/upload-dev-private", jsonStr)

    print(jResp)

    # challenge (4096 bit)
    challenge = "51439ed8d7cbdd5f2d6c7d3430f6be97326dadc3da0d8a071381095bb20fb90b"
    challenge += "502f2ca8818436cbce581293e1864d7f85944454fe0361b1e9f0657164771f39"
    challenge += "1c11ab38030d65cadfa2d6dc40e6abb3ba3f444f242d8d330d934be375471321"
    challenge += "238e4bb18941120c902d2366136ac26c073c6229ce46ff1ee7f681133ce74fce"
    challenge += "e1e53ddc12ff62acefb076db1490229061508c3a54d1bba826f2bd95c4780100"
    challenge += "6ebdb174b386deb253505ae650d70aee8aebf866eecfa376eb568f3e8dd58f92"
    challenge += "feddc605251b126fcfae0f21008d98fe593f458971a03ac25eb7aab10c60c6af"
    challenge += "8f8605449d34f6ed647bb05edf919145a23d822b55a47309dd27e901d922b021"
    challenge += "dc18bd9221baf722909d9d27e982bb2e70c1aa926633b512f2f1871d59a11017"
    challenge += "e6321fcc84459fbb9c47381afd4710bd1d1777cf187aa00533451e52636bddea"
    challenge += "81a5d02500202716b30be119915add1e26b2e2c08beb743a008a58fbc4cfc31f"
    challenge += "d361a4e0e56ccd3cae4a0658a1281f89dec634d4079971dacca5fb78b8ed0984"
    challenge += "3a7ba88ba0751c9d39be78ead9d0609176c65830ad6e7391d164f98865b3bd0e"
    challenge += "5f1625810c4c9e6be8e023fbe9a87695b7e1ae0ea05964c96af684f5a3c5276b"
    challenge += "5c005c573f1fd24f28b095467a2edbf918a4e23a27acadf280b43ff484187098"
    challenge += "e6c4bf5bc43e6447b2daeca4ae35572412adfe91bffc86de2a8dca15207589a9"

    challenge = int(challenge,base=16)

    print("\nchallenge: " + hex(challenge) + "\n")

    # expected signature
    sigExpected = rsaSign(challenge, priv_numbers.d, pub_numbers.n)
    print("\nrsaSign: " + hex(sigExpected) + "\n")

    # sign chalenge
    j = '{"challenge512":"' + hex(challenge).replace("0x","") + '"}'
    jResp = jflashConsole.invoke_json_cmd(st,"/api/auth/esp/challenge", j)
    print(jResp)

    if jResp['signature'] != hex(sigExpected).replace("0x",""):
        print("verify: failed");

uploading private key:

cJSON* pd_api_auth_upload_esp_dev_private(const pd_4096_bit_t* rsaY, 
                                          const pd_4096_bit_t* rsaM, 
                                          const pd_4096_bit_t* rsaRb, 
                                                      uint32_t rsaMPrime, 
                                                  pd_error_t* error)
{
    pd_err_unset(error);

    // allcations
    cJSON* jRoot = NULL;
    esp_ds_p_data_t* params = NULL;
    esp_ds_data_t* encrypted = NULL;

    {
    // set params
    params = (esp_ds_p_data_t*) calloc_safe2(MALLOC_CAP_DMA, sizeof(esp_ds_p_data_t), 1, HERE);
    params->length = ESP_DS_RSA_4096;
    // convert from big-endian to native-endian
    for(int i = 0; i < (ESP_DS_RSA_4096+1); i++){
        params->Y[i] |= rsaY->bytes[(i*4)+0] << 24;
        params->Y[i] |= rsaY->bytes[(i*4)+1] << 16;
        params->Y[i] |= rsaY->bytes[(i*4)+2] << 8;
        params->Y[i] |= rsaY->bytes[(i*4)+3] << 0;

        params->M[i] |= rsaM->bytes[(i*4)+0] << 24;
        params->M[i] |= rsaM->bytes[(i*4)+1] << 16;
        params->M[i] |= rsaM->bytes[(i*4)+2] << 8;
        params->M[i] |= rsaM->bytes[(i*4)+3] << 0;

        params->Rb[i] |= rsaRb->bytes[(i*4)+0] << 24;
        params->Rb[i] |= rsaRb->bytes[(i*4)+1] << 16;
        params->Rb[i] |= rsaRb->bytes[(i*4)+2] << 8;
        params->Rb[i] |= rsaRb->bytes[(i*4)+3] << 0;
    }
    params->M_prime = rsaMPrime;

    // allcate
    encrypted = (esp_ds_data_t*) calloc_safe2(MALLOC_CAP_DMA, sizeof(esp_ds_data_t), 1, HERE);

    // iv
    uint8_t iv[16];
    esp_fill_random(iv, 16);

    // hmac
    uint8_t hmac[32];
    esp_fill_random(hmac, 32);

    // aes encrypt RSA params with IV & HMAC keys
    esp_err_t err = esp_ds_encrypt_params(encrypted, iv, params, hmac);
    if (err != ESP_OK){
        pd_err_fail(error, TAG, "esp_ds_encrypt_params() failed. %s", esp_err_to_name(err));
        goto end;
    }

    // store encrypted RSA key into nvs
    pd_nvs_esp_ds_cipher_set(encrypted->c, ESP_DS_C_LEN);
    pd_nvs_esp_ds_iv_set(iv, ESP_DS_IV_LEN);

    // burn hmac key into fuses
    err = esp_efuse_write_key(EFUSE_BLK_KEY0, 
        ESP_EFUSE_KEY_PURPOSE_HMAC_DOWN_DIGITAL_SIGNATURE, hmac, 32);
    if (err != ESP_OK){
        pd_err_fail(error, TAG, "esp_efuse_write_key() failed. %s", esp_err_to_name(err));
        goto end;
    }

    jRoot = cJSON_CreateObject();
    cJSON_AddBoolToObject(jRoot, "burned", true);

    pd_err_success(error);
    }
end:
    free_safe(params);
    free_safe(encrypted);

    if (is_fail(*error)){
        cJSON_Delete(jRoot);
        return NULL;
    }

    return jRoot;
}

challenge:

cJSON* pd_api_auth_esp_challenge(const pd_4096_bit_t* challenge, pd_error_t* error)
{
 pd_err_unset(error);

    // Allocations
    cJSON* jRoot = NULL;
    char* sigHex = NULL;
    uint8_t* cipher = NULL;
    uint8_t* iv = NULL;
    uint8_t* signature = NULL;

    {
    // get cipher
    uint32_t len = 0;
    uint8_t* cipher = pd_nvs_esp_ds_cipher_get(&len);
    if (cipher == NULL) {
        pd_err_fail(error, TAG, "cipher is not set");
        goto end;
    }

    if (len != ESP_DS_C_LEN) {
        pd_err_fail(error, TAG, "cipher len (%u) should be %u", len, ESP_DS_C_LEN);
        goto end;
    }

    // get iv
    len = 0;
    uint8_t* iv = pd_nvs_esp_ds_iv_get(&len);
    if (iv == NULL) {
        pd_err_fail(error, TAG, "iv is not set");
        goto end;
    }

    if (len != ESP_DS_IV_LEN) {
        pd_err_fail(error, TAG, "iv len (%u) should be %u", len, ESP_DS_IV_LEN);
        goto end;
    }

    // create ds data
    esp_ds_data_t data;
    data.rsa_length = ESP_DS_RSA_4096;
    memcpy(data.iv, iv, ESP_DS_IV_LEN);
    memcpy(data.c, cipher, ESP_DS_C_LEN);

    // signature
    uint32_t sigLen = (data.rsa_length+1) * 4;
    signature = (uint8_t*) calloc_safe(sigLen, 1, HERE);

    pd_4096_bit_t challengeB;
    memcpy(challengeB.bytes, challenge->bytes, 4096/8);

   // big endian -> little endian
    for (unsigned int i = 0; i < 4096/32; i++) {
        challengeB.bytes[i] = SWAP_INT32(((uint32_t *)challengeB.bytes)[(4096/32) - (i + 1)]);
    }

    // ds sign
    esp_err_t err = esp_ds_sign(challengeB->bytes, &data, HMAC_KEY0, signature);
    if (err != ESP_OK){
        pd_err_fail(error, TAG, "esp_ds_sign() failed. %s", esp_err_to_name(err));
        goto end;
    }

    // hex encode
    sigHex = hexEncode_alloc(signature, sigLen);

    jRoot = cJSON_CreateObject();
    cJSON_AddStringToObject(jRoot, "signature", sigHex);

    pd_err_success(error);
    }
end:
    free_safe(sigHex);
    free_safe(cipher);
    free_safe(iv);
    free_safe(signature);

    if (is_fail(*error)){
        cJSON_Delete(jRoot);
        return NULL;
    }

    return jRoot;
}
chipweinberger commented 1 year ago

Each device has 5 Key slots, so... I suppose I can test 5 times per board. Which is not...too bad.

chipweinberger commented 1 year ago

@mahavirj

Could you look at the code and see if there is anything obviously wrong? The RSA signatures I am generating on the device do not match the signature I'm generating in python.

I think my usage of esp_ds_encrypt_params() or esp_ds_sign() might be wrong.

chipweinberger commented 1 year ago

Closing this issue in favor of a new one.

https://github.com/espressif/esp-idf/issues/10028

chipweinberger commented 1 year ago

And for the record, virtually fuses are not supported.

They cause ESP_ERR_HW_CRYPTO_DS_HMAC_FAIL

KonstantinKondrashov commented 1 year ago

Hi @chipweinberger! Sorry, It is not possible to support the Virtual eFuses because the DS module uses hardware to get the result.

In case you use the Virtual eFuses with DS you will get ESP_ERR_HW_CRYPTO_DS_HMAC_FAIL because the DS hardware reads a given eFuse key block but it is empty, eFuses are stored in RAM, not in eFuse registers, hardware can not reach it. There is no way you need to use the real eFuse mode.

chipweinberger commented 1 year ago

Understood.

it would be great if a test function simulated the DS hardware with similar API, using virtual fuses.

But that it just a request.

With careful planning, I was able to get the DS hardware working with 5 attempts, and 5 burned keys.

mahavirj commented 1 year ago

@chipweinberger

it would be great if a test function simulated the DS hardware with similar API, using virtual fuses.

What might help here is to have more targets (includes ESP32-S2/C2/S3) supported in our qemu fork here, that way it would be possible to easily try out security features under emulator first and then move to the real physical target. Currently we have only ESP32 support in this but it does not feature DS peripheral. We will try to prioritize support for other targets here.

Virtual efuse feature is mostly for our internal testing purpose, but it definitely can not help in scenarios where peripheral is actually expecting credentials/data from actual efuse block/s. We will add note regarding this in our docs. Thank you for the feedback.

chipweinberger commented 1 year ago

Yes that would be useful. Not all users will be able to use (easily) the simulator due to other hardware constraints, though.

What I am proposing it to completely reimplement the DS hardware in software in ESP-IDF. The software could then read the virtual fuses. I admit, It might be more trouble than it is worth.

Something like this:

#ifdef CONFIG_VIRTUAL_FUSES
    ds_software_hal_start(key_id);
#elseif
     ds_hal_start();
#endif

I'm not an expert, but the the RSA algorithm is not that complex. The software might be reasonable to maintain. Perhaps you can use mbedtls.