Skip to content

Commit

Permalink
[crypto] Add OAEP padding for RSA.
Browse files Browse the repository at this point in the history
This padding mode is used for RSA encryption and is defined in IETF RFC
8017.

Signed-off-by: Jade Philipoom <[email protected]>
  • Loading branch information
jadephilipoom committed Dec 22, 2023
1 parent 2359e6d commit 6aa7fb0
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 8 deletions.
5 changes: 5 additions & 0 deletions sw/device/lib/crypto/drivers/entropy.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
// Module ID for status codes.
#define MODULE_ID MAKE_MODULE_ID('d', 'e', 'n')

const entropy_seed_material_t kEntropyEmptySeed = {
.len = 0,
.data = {0},
};

enum {
kBaseCsrng = TOP_EARLGREY_CSRNG_BASE_ADDR,
kBaseEntropySrc = TOP_EARLGREY_ENTROPY_SRC_BASE_ADDR,
Expand Down
7 changes: 7 additions & 0 deletions sw/device/lib/crypto/drivers/entropy.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ typedef struct entropy_seed_material {
uint32_t data[kEntropySeedWords];
} entropy_seed_material_t;

/**
* Constant empty seed material for the entropy complex.
*
* This is convenient to have available for some implementations.
*/
extern const entropy_seed_material_t kEntropyEmptySeed;

/**
* Configures the entropy complex in continuous mode.
*
Expand Down
1 change: 1 addition & 0 deletions sw/device/lib/crypto/impl/rsa/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ cc_library(
"//sw/device/lib/base:hardened",
"//sw/device/lib/base:hardened_memory",
"//sw/device/lib/base:memory",
"//sw/device/lib/crypto/drivers:entropy",
"//sw/device/lib/crypto/impl:hash",
"//sw/device/lib/crypto/impl:status",
"//sw/device/lib/crypto/impl/sha2:sha256",
Expand Down
234 changes: 234 additions & 0 deletions sw/device/lib/crypto/impl/rsa/rsa_padding.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "sw/device/lib/base/hardened.h"
#include "sw/device/lib/base/hardened_memory.h"
#include "sw/device/lib/base/math.h"
#include "sw/device/lib/crypto/drivers/entropy.h"
#include "sw/device/lib/crypto/impl/sha2/sha256.h"
#include "sw/device/lib/crypto/impl/sha2/sha512.h"
#include "sw/device/lib/crypto/include/hash.h"
Expand Down Expand Up @@ -459,3 +460,236 @@ status_t rsa_padding_pss_verify(const hash_digest_t *message_digest,
*result = hardened_memeq(h, exp_h, ARRAYSIZE(exp_h));
return OTCRYPTO_OK;
}

status_t rsa_padding_oaep_max_message_bytelen(const hash_mode_t hash_mode,
size_t rsa_wordlen,
size_t *max_message_bytelen) {
// Get the hash digest length for the given hash function (and check that it
// is one of the supported hash functions).
size_t digest_wordlen = 0;
HARDENED_TRY(digest_wordlen_get(hash_mode, &digest_wordlen));

size_t digest_bytelen = digest_wordlen * sizeof(uint32_t);
size_t rsa_bytelen = rsa_wordlen * sizeof(uint32_t);
if (2 * digest_bytelen + 2 > rsa_bytelen) {
// This case would cause underflow if we continue; return an error.
return OTCRYPTO_BAD_ARGS;
}

*max_message_bytelen = rsa_bytelen - 2 * digest_bytelen - 2;
return OTCRYPTO_OK;
}

status_t rsa_padding_oaep_encode(const hash_mode_t hash_mode,
const uint8_t *message, size_t message_bytelen,
const uint8_t *label, size_t label_bytelen,
size_t encoded_message_len,
uint32_t *encoded_message) {
// Check that the message is not too long (RFC 8017, section 7.1.1, step 1a).
size_t max_message_bytelen = 0;
HARDENED_TRY(rsa_padding_oaep_max_message_bytelen(
hash_mode, encoded_message_len, &max_message_bytelen));
if (message_bytelen > max_message_bytelen) {
return OTCRYPTO_BAD_ARGS;
}

// Get the hash digest length for the given hash function (and check that it
// is one of the supported hash functions).
size_t digest_wordlen = 0;
HARDENED_TRY(digest_wordlen_get(hash_mode, &digest_wordlen));

// Hash the label (step 2a).
crypto_const_byte_buf_t label_buf = {
.data = label,
.len = label_bytelen,
};
uint32_t lhash_data[digest_wordlen];
hash_digest_t lhash = {
.data = lhash_data,
.len = ARRAYSIZE(lhash_data),
.mode = hash_mode,
};
HARDENED_TRY(otcrypto_hash(label_buf, &lhash));

// Generate a random string the same length as a hash digest (step 2d).
uint32_t seed[digest_wordlen];
HARDENED_TRY(entropy_complex_check());
HARDENED_TRY(entropy_csrng_instantiate(
/*disable_trng_input=*/kHardenedBoolFalse, &kEntropyEmptySeed));
HARDENED_TRY(entropy_csrng_generate(&kEntropyEmptySeed, seed, ARRAYSIZE(seed),
/*fips_check=*/kHardenedBoolTrue));
HARDENED_TRY(entropy_csrng_uninstantiate());

// Generate dbMask = MGF(seed, k - hLen - 1) (step 2e).
size_t digest_bytelen = digest_wordlen * sizeof(uint32_t);
size_t encoded_message_bytelen = encoded_message_len * sizeof(uint32_t);
size_t db_bytelen = encoded_message_bytelen - digest_bytelen - 1;
size_t db_wordlen = ceil_div(db_bytelen, sizeof(uint32_t));
uint32_t db[db_wordlen];
HARDENED_TRY(
mgf1(hash_mode, (unsigned char *)seed, sizeof(seed), db_bytelen, db));

// Construct maskedDB = dbMask XOR (lhash || PS || 0x01 || M), where PS is
// all-zero (step 2f). By computing the mask first, we can simply XOR with
// lhash, 0x01, and M, skipping PS because XOR with zero is the identity
// function.
for (size_t i = 0; i < ARRAYSIZE(lhash_data); i++) {
db[i] ^= lhash_data[i];
}
size_t message_start_idx = db_bytelen - message_bytelen;
unsigned char *db_bytes = (unsigned char *)db;
db_bytes[message_start_idx - 1] ^= 0x01;
for (size_t i = 0; i < message_bytelen; i++) {
db_bytes[message_start_idx + i] ^= message[i];
}

// Compute seedMask = MGF(maskedDB, hLen) (step 2g).
uint32_t seed_mask[digest_wordlen];
HARDENED_TRY(mgf1(hash_mode, (unsigned char *)db, db_bytelen, digest_bytelen,
seed_mask));

// Construct maskedSeed = seed XOR seedMask (step 2h).
for (size_t i = 0; i < ARRAYSIZE(seed); i++) {
seed[i] ^= seed_mask[i];
}

// Construct EM = 0x00 || maskedSeed || maskedDB (step 2i).
unsigned char *encoded_message_bytes = (unsigned char *)encoded_message;
encoded_message_bytes[0] = 0x00;
memcpy(encoded_message_bytes + 1, seed, sizeof(seed));
memcpy(encoded_message_bytes + 1 + sizeof(seed), db, sizeof(db));

// Reverse the byte-order.
reverse_bytes(encoded_message_len, encoded_message);
return OTCRYPTO_OK;
}

status_t rsa_padding_oaep_decode(const hash_mode_t hash_mode,
const uint8_t *label, size_t label_bytelen,
uint32_t *encoded_message,
size_t encoded_message_len, uint8_t *message,
size_t *message_bytelen) {
// Reverse the byte-order.
reverse_bytes(encoded_message_len, encoded_message);
*message_bytelen = 0;

// Get the hash digest length for the given hash function (and check that it
// is one of the supported hash functions).
size_t digest_wordlen = 0;
HARDENED_TRY(digest_wordlen_get(hash_mode, &digest_wordlen));

// Extract maskedSeed from the encoded message (RFC 8017, section 7.1.2, step
// 3b).
uint32_t seed[digest_wordlen];
unsigned char *encoded_message_bytes = (unsigned char *)encoded_message;
memcpy(seed, encoded_message_bytes + 1, sizeof(seed));

// Extract maskedDB from the encoded message (RFC 8017, section 7.1.2, step
// 3b).
size_t digest_bytelen = digest_wordlen * sizeof(uint32_t);
size_t encoded_message_bytelen = encoded_message_len * sizeof(uint32_t);
size_t db_bytelen = encoded_message_bytelen - digest_bytelen - 1;
size_t db_wordlen = ceil_div(db_bytelen, sizeof(uint32_t));
uint32_t db[db_wordlen];
memcpy(db, encoded_message_bytes + 1 + sizeof(seed), db_bytelen);

// Compute seedMask = MGF(maskedDB, hLen) (step 3c).
uint32_t seed_mask[digest_wordlen];
HARDENED_TRY(mgf1(hash_mode, (unsigned char *)db, db_bytelen, digest_bytelen,
seed_mask));

// Construct seed = maskedSeed XOR seedMask (step 3d).
for (size_t i = 0; i < ARRAYSIZE(seed); i++) {
seed[i] ^= seed_mask[i];
}

// Generate dbMask = MGF(seed, k - hLen - 1) (step 3e).
uint32_t db_mask[db_wordlen];
HARDENED_TRY(mgf1(hash_mode, (unsigned char *)seed, sizeof(seed), db_bytelen,
db_mask));

// Zero trailing bytes of DB and dbMask if needed.
size_t num_trailing_bytes = sizeof(db) - db_bytelen;
if (num_trailing_bytes > 0) {
memset(((unsigned char *)db) + db_bytelen, 0, num_trailing_bytes);
memset(((unsigned char *)db_mask) + db_bytelen, 0, num_trailing_bytes);
}

// Construct DB = dbMask XOR maskedDB.
for (size_t i = 0; i < ARRAYSIZE(db); i++) {
db[i] ^= db_mask[i];
}

// Hash the label (step 3a).
crypto_const_byte_buf_t label_buf = {
.data = label,
.len = label_bytelen,
};
uint32_t lhash_data[digest_wordlen];
hash_digest_t lhash = {
.data = lhash_data,
.len = digest_wordlen,
.mode = hash_mode,
};
HARDENED_TRY(otcrypto_hash(label_buf, &lhash));

// Note: as we compare parts of the encoded message to their expected values,
// we must be careful that the attacker cannot differentiate error codes or
// get partial information about the encoded message. See the note in RCC
// 8017, section 7.1.2. This implementation currently protects against
// revealing this information through error codes or timing, but does not yet
// defend against power side channels.

// Locate the start of the message in DB = lhash || 0x00..0x00 || 0x01 || M
// by searching for the 0x01 byte in constant time.
unsigned char *db_bytes = (unsigned char *)db;
uint32_t message_start_idx = 0;
ct_bool32_t decode_failure = 0;
for (size_t i = digest_bytelen; i < db_bytelen; i++) {
uint32_t byte = 0;
memcpy(&byte, db_bytes + i, 1);
ct_bool32_t is_one = ct_seq32(byte, 0x01);
ct_bool32_t is_before_message = ct_seqz32(message_start_idx);
ct_bool32_t is_message_start = is_one & is_before_message;
message_start_idx = ct_cmov32(is_message_start, i + 1, message_start_idx);
ct_bool32_t is_zero = ct_seqz32(byte);
ct_bool32_t padding_failure = is_before_message & (~is_zero) & (~is_one);
decode_failure |= padding_failure;
}
HARDENED_CHECK_LE(message_start_idx, db_bytelen);

// If we never found a message start index, we should fail. However, don't
// fail yet to avoid leaking timing information.
ct_bool32_t message_start_not_found = ct_seqz32(message_start_idx);
decode_failure |= message_start_not_found;

// Check that the first part of DB is equal to lhash.
hardened_bool_t lhash_matches =
hardened_memeq(lhash_data, db, digest_wordlen);
ct_bool32_t lhash_match = ct_seq32(lhash_matches, kHardenedBoolTrue);
ct_bool32_t lhash_mismatch = ~lhash_match;
decode_failure |= lhash_mismatch;

// Check that the leading byte is 0.
uint32_t leading_byte = 0;
memcpy(&leading_byte, encoded_message_bytes, 1);
ct_bool32_t leading_byte_nonzero = ~ct_seqz32(leading_byte);
decode_failure |= leading_byte_nonzero;

// Now, decode_failure is all-zero if the decode succeeded and all-one if the
// decode failed.
if (launder32(decode_failure) != 0) {
return OTCRYPTO_BAD_ARGS;
}
HARDENED_CHECK_EQ(decode_failure, 0);

// TODO: re-check the padding as an FI hardening measure?

// If we get here, then the encoded message has a proper format and it is
// safe to copy the message into the output buffer.
*message_bytelen = db_bytelen - message_start_idx;
if (*message_bytelen > 0) {
memcpy(message, db_bytes + message_start_idx, *message_bytelen);
}
return OTCRYPTO_OK;
}
87 changes: 87 additions & 0 deletions sw/device/lib/crypto/impl/rsa/rsa_padding.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,93 @@ status_t rsa_padding_pss_verify(const hash_digest_t *message_digest,
size_t encoded_message_len,
hardened_bool_t *result);

/**
* Maximum message byte-length for OAEP padding.
*
* As per RFC 8017, the maximum message byte-length for OAEP is k - 2*hLen - 2,
* where k is the size of the RSA modulus in bytes and hLen is the digest
* length of the hash function used for padding. This function provides a
* simple convenience interface so that callers can check that buffers for
* decoded messages are long enough.
*
* Returns an error if the hash mode is not supported (i.e., a non-fixed-length
* hash function).
*
* @param hash_mode Hash function to use for OAEP.
* @param rsa_wordlen RSA modulus size in 32-bit words.
* @param[out] max_message_bytelen Maximum length of message in bytes.
* @return Result of the operation (OK or error).
*/
status_t rsa_padding_oaep_max_message_bytelen(const hash_mode_t hash_mode,
size_t rsa_wordlen,
size_t *max_message_bytelen);

/**
* Encode the message with OAEP encoding (RFC 8017, section 7.1.1, steps 1-2).
*
* The caller must ensure that `encoded_message_len` 32-bit words are allocated
* in the output buffer.
*
* The maximum byte-length of the message (as per the RFC) is k - 2*hLen - 2,
* where k is the RSA size and hLen is the length of the hash digest. This
* function will return an error if the message is too long.
*
* The hash function must be a fixed-length (SHA-2 or SHA-3) hash function. The
* MGF will always be MGF1 with the same hash function.
*
* We encode the message in reversed byte-order from the RFC because OTBN
* interprets the message as a fully little-endian integer.
*
* @param hash_mode Hash function to use.
* @param message Message to encode.
* @param message_bytelen Message length in bytes.
* @param label Label for OAEP.
* @param label_bytelen Label length in bytes.
* @param encoded_message_len Intended encoded message length in 32-bit words.
* @param[out] encoded_message Encoded message.
* @return Result of the operation (OK or error).
*/
OT_WARN_UNUSED_RESULT
status_t rsa_padding_oaep_encode(const hash_mode_t hash_mode,
const uint8_t *message, size_t message_bytelen,
const uint8_t *label, size_t label_bytelen,
size_t encoded_message_len,
uint32_t *encoded_message);

/**
* Decode the OAEP-encoded message (RFC 8017, section 7.1.2, step 3).
*
* The maximum byte-length of the message (as per the RFC) is k - 2*hLen - 2,
* where k is the RSA size and hLen is the length of the hash digest. The
* caller must ensure there are at least this many bytes available for
* `message`; they can call `rsa_padding_oaep_max_message_bytelen` to get the
* exact value for a given hash mode and RSA size.
*
* The hash function must be a fixed-length (SHA-2 or SHA-3) hash function. The
* MGF will always be MGF1 with the same hash function.
*
* Note that this function expects the encoded message in reversed byte-order
* compared to the RFC, since OTBN is little-endian.
*
* Warning: modifies the encoded message in-place during comparison
* (specifically, reverses the byte-order).
*
* @param hash_mode Hash function to use.
* @param label Label for OAEP.
* @param label_bytelen Label length in bytes.
* @param encoded_message Encoded message.
* @param encoded_message_len Encoded message length in 32-bit words.
* @param[out] message Decoded message.
* @param[out] message_bytelen Length of the message in bytes.
* @return Result of the operation (OK or error).
*/
OT_WARN_UNUSED_RESULT
status_t rsa_padding_oaep_decode(const hash_mode_t hash_mode,
const uint8_t *label, size_t label_bytelen,
uint32_t *encoded_message,
size_t encoded_message_len, uint8_t *message,
size_t *message_bytelen);

#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
Expand Down
8 changes: 0 additions & 8 deletions sw/device/lib/crypto/impl/rsa/rsa_signature.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@
// Module ID for status codes.
#define MODULE_ID MAKE_MODULE_ID('r', 's', 'v')

/**
* Constant empty seed material for the entropy complex.
*/
static const entropy_seed_material_t kEntropyEmptySeed = {
.len = 0,
.data = {0},
};

/**
* Ensure that the digest type matches the length and is supported.
*
Expand Down

0 comments on commit 6aa7fb0

Please sign in to comment.