Skip to content

Commit

Permalink
[nrf fromtree][Crypto] Add HMAC function using KeyHandle (#30751)
Browse files Browse the repository at this point in the history
* Add new API for HMAC with key handle

* Rename Aes128BitsKeyHandle to Aes128KeyHandle

* Rename Hmac128BitsKeyHandle to Hmac128KeyHandle

* Replace virtual destructor with a protected one

* key algo creation

---------

Co-authored-by: Mathieu Kardous <mathieu.kardous@silabs.com>
  • Loading branch information
2 people authored and kkasperczyk-no committed Feb 16, 2024
1 parent 2bab4db commit bdded49
Show file tree
Hide file tree
Showing 26 changed files with 248 additions and 111 deletions.
2 changes: 1 addition & 1 deletion src/app/icd/ICDMonitoringTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ struct ICDMonitoringEntry : public PersistentData<kICDMonitoringBufferSize>
chip::FabricIndex fabricIndex = kUndefinedFabricIndex;
chip::NodeId checkInNodeID = kUndefinedNodeId;
uint64_t monitoredSubject = static_cast<uint64_t>(0);
Crypto::Aes128BitsKeyHandle key = Crypto::Aes128BitsKeyHandle();
Crypto::Aes128KeyHandle key = Crypto::Aes128KeyHandle();
bool keyHandleValid = false;
uint16_t index = 0;
Crypto::SymmetricKeystore * symmetricKeystore = nullptr;
Expand Down
4 changes: 2 additions & 2 deletions src/credentials/GroupDataProviderImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ class GroupDataProviderImpl : public GroupDataProvider
protected:
GroupDataProviderImpl & mProvider;
uint16_t mKeyHash = 0;
Crypto::Aes128BitsKeyHandle mEncryptionKey;
Crypto::Aes128BitsKeyHandle mPrivacyKey;
Crypto::Aes128KeyHandle mEncryptionKey;
Crypto::Aes128KeyHandle mPrivacyKey;
};

class KeySetIteratorImpl : public KeySetIterator
Expand Down
7 changes: 1 addition & 6 deletions src/crypto/CHIPCryptoPAL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,6 @@ CHIP_ERROR Find16BitUpperCaseHexAfterPrefix(const ByteSpan & buffer, const char

} // namespace

Symmetric128BitsKeyHandle::~Symmetric128BitsKeyHandle()
{
ClearSecretData(mContext.mOpaque);
}

using HKDF_sha_crypto = HKDF_sha;

CHIP_ERROR Spake2p::InternalHash(const uint8_t * in, size_t in_len)
Expand Down Expand Up @@ -784,7 +779,7 @@ CHIP_ERROR EcdsaAsn1SignatureToRaw(size_t fe_length_bytes, const ByteSpan & asn1
return CHIP_NO_ERROR;
}

CHIP_ERROR AES_CTR_crypt(const uint8_t * input, size_t input_length, const Aes128BitsKeyHandle & key, const uint8_t * nonce,
CHIP_ERROR AES_CTR_crypt(const uint8_t * input, size_t input_length, const Aes128KeyHandle & key, const uint8_t * nonce,
size_t nonce_length, uint8_t * output)
{
// Discard tag portion of CCM to apply only CTR mode encryption/decryption.
Expand Down
67 changes: 45 additions & 22 deletions src/crypto/CHIPCryptoPAL.h
Original file line number Diff line number Diff line change
Expand Up @@ -574,15 +574,11 @@ using Symmetric128BitsKeyByteArray = uint8_t[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BY
*
* @note Symmetric128BitsKeyHandle is an abstract class to force child classes for each key handle type.
* Symmetric128BitsKeyHandle class implements all the necessary components for handles.
* Child classes only need to implement a constructor, implement a destructor and delete all the copy operators.
* Child classes only need to implement a constructor and delete all the copy operators.
*/
class Symmetric128BitsKeyHandle
{
public:
Symmetric128BitsKeyHandle() = default;
// Destructor is implemented in the .cpp. It is pure virtual only to force the class to be abstract.
virtual ~Symmetric128BitsKeyHandle() = 0;

Symmetric128BitsKeyHandle(const Symmetric128BitsKeyHandle &) = delete;
Symmetric128BitsKeyHandle(Symmetric128BitsKeyHandle &&) = delete;
void operator=(const Symmetric128BitsKeyHandle &) = delete;
Expand All @@ -606,6 +602,10 @@ class Symmetric128BitsKeyHandle
return *SafePointerCast<T *>(&mContext);
}

protected:
Symmetric128BitsKeyHandle() = default;
~Symmetric128BitsKeyHandle() { ClearSecretData(mContext.mOpaque); }

private:
static constexpr size_t kContextSize = CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES;

Expand All @@ -618,31 +618,29 @@ class Symmetric128BitsKeyHandle
/**
* @brief Platform-specific AES key handle
*/
class Aes128BitsKeyHandle : public Symmetric128BitsKeyHandle
class Aes128KeyHandle final : public Symmetric128BitsKeyHandle
{
public:
Aes128BitsKeyHandle() = default;
virtual ~Aes128BitsKeyHandle() {}
Aes128KeyHandle() = default;

Aes128BitsKeyHandle(const Aes128BitsKeyHandle &) = delete;
Aes128BitsKeyHandle(Aes128BitsKeyHandle &&) = delete;
void operator=(const Aes128BitsKeyHandle &) = delete;
void operator=(Aes128BitsKeyHandle &&) = delete;
Aes128KeyHandle(const Aes128KeyHandle &) = delete;
Aes128KeyHandle(Aes128KeyHandle &&) = delete;
void operator=(const Aes128KeyHandle &) = delete;
void operator=(Aes128KeyHandle &&) = delete;
};

/**
* @brief Platform-specific HMAC key handle
*/
class Hmac128BitsKeyHandle : public Symmetric128BitsKeyHandle
class Hmac128KeyHandle final : public Symmetric128BitsKeyHandle
{
public:
Hmac128BitsKeyHandle() = default;
virtual ~Hmac128BitsKeyHandle() {}
Hmac128KeyHandle() = default;

Hmac128BitsKeyHandle(const Hmac128BitsKeyHandle &) = delete;
Hmac128BitsKeyHandle(Hmac128BitsKeyHandle &&) = delete;
void operator=(const Hmac128BitsKeyHandle &) = delete;
void operator=(Hmac128BitsKeyHandle &&) = delete;
Hmac128KeyHandle(const Hmac128KeyHandle &) = delete;
Hmac128KeyHandle(Hmac128KeyHandle &&) = delete;
void operator=(const Hmac128KeyHandle &) = delete;
void operator=(Hmac128KeyHandle &&) = delete;
};

/**
Expand Down Expand Up @@ -732,7 +730,7 @@ CHIP_ERROR ConvertIntegerRawToDerWithoutTag(const ByteSpan & raw_integer, Mutabl
* @return Returns a CHIP_ERROR on error, CHIP_NO_ERROR otherwise
* */
CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, const uint8_t * aad, size_t aad_length,
const Aes128BitsKeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
const Aes128KeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
uint8_t * tag, size_t tag_length);

/**
Expand All @@ -756,7 +754,7 @@ CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, c
* @return Returns a CHIP_ERROR on error, CHIP_NO_ERROR otherwise
**/
CHIP_ERROR AES_CCM_decrypt(const uint8_t * ciphertext, size_t ciphertext_length, const uint8_t * aad, size_t aad_length,
const uint8_t * tag, size_t tag_length, const Aes128BitsKeyHandle & key, const uint8_t * nonce,
const uint8_t * tag, size_t tag_length, const Aes128KeyHandle & key, const uint8_t * nonce,
size_t nonce_length, uint8_t * plaintext);

/**
Expand All @@ -775,7 +773,7 @@ CHIP_ERROR AES_CCM_decrypt(const uint8_t * ciphertext, size_t ciphertext_length,
* @param output Buffer to write output into
* @return Returns a CHIP_ERROR on error, CHIP_NO_ERROR otherwise
**/
CHIP_ERROR AES_CTR_crypt(const uint8_t * input, size_t input_length, const Aes128BitsKeyHandle & key, const uint8_t * nonce,
CHIP_ERROR AES_CTR_crypt(const uint8_t * input, size_t input_length, const Aes128KeyHandle & key, const uint8_t * nonce,
size_t nonce_length, uint8_t * output);

/**
Expand Down Expand Up @@ -982,6 +980,31 @@ class HMAC_sha

virtual CHIP_ERROR HMAC_SHA256(const uint8_t * key, size_t key_length, const uint8_t * message, size_t message_length,
uint8_t * out_buffer, size_t out_length);

/**
* @brief A function that implements SHA-256 based HMAC per FIPS1981.
*
* This implements the CHIP_Crypto_HMAC() cryptographic primitive
* in the the specification.
*
* The `out_length` must be at least kSHA256_Hash_Length, and only
* kSHA256_Hash_Length bytes are written to out_buffer.
*
* Error values are:
* - CHIP_ERROR_INVALID_ARGUMENT: for any bad arguments or nullptr input on
* any pointer.
* - CHIP_ERROR_INTERNAL: for any unexpected error arising in the underlying
* cryptographic layers.
*
* @param key The HMAC Key handle to use for the HMAC operation
* @param message Message over which to compute the HMAC
* @param message_length Length of the message over which to compute the HMAC
* @param out_buffer Pointer to buffer into which to write the output.
* @param out_length Underlying size of the `out_buffer`.
* @return Returns a CHIP_ERROR on error, CHIP_NO_ERROR otherwise
**/
virtual CHIP_ERROR HMAC_SHA256(const Hmac128KeyHandle & key, const uint8_t * message, size_t message_length,
uint8_t * out_buffer, size_t out_length);
};

/**
Expand Down
11 changes: 9 additions & 2 deletions src/crypto/CHIPCryptoPALOpenSSL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ static int _compareDaysAndSeconds(const int days, const int seconds)
}

CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, const uint8_t * aad, size_t aad_length,
const Aes128BitsKeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
const Aes128KeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
uint8_t * tag, size_t tag_length)
{
#if CHIP_CRYPTO_BORINGSSL
Expand Down Expand Up @@ -282,7 +282,7 @@ CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, c
}

CHIP_ERROR AES_CCM_decrypt(const uint8_t * ciphertext, size_t ciphertext_length, const uint8_t * aad, size_t aad_length,
const uint8_t * tag, size_t tag_length, const Aes128BitsKeyHandle & key, const uint8_t * nonce,
const uint8_t * tag, size_t tag_length, const Aes128KeyHandle & key, const uint8_t * nonce,
size_t nonce_length, uint8_t * plaintext)
{
#if CHIP_CRYPTO_BORINGSSL
Expand Down Expand Up @@ -598,6 +598,13 @@ CHIP_ERROR HMAC_sha::HMAC_SHA256(const uint8_t * key, size_t key_length, const u
return error;
}

CHIP_ERROR HMAC_sha::HMAC_SHA256(const Hmac128KeyHandle & key, const uint8_t * message, size_t message_length, uint8_t * out_buffer,
size_t out_length)
{
return HMAC_SHA256(key.As<Symmetric128BitsKeyByteArray>(), sizeof(Symmetric128BitsKeyByteArray), message, message_length,
out_buffer, out_length);
}

CHIP_ERROR PBKDF2_sha256::pbkdf2_sha256(const uint8_t * password, size_t plen, const uint8_t * salt, size_t slen,
unsigned int iteration_count, uint32_t key_length, uint8_t * output)
{
Expand Down
19 changes: 17 additions & 2 deletions src/crypto/CHIPCryptoPALPSA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ bool isValidTag(const uint8_t * tag, size_t tag_length)
} // namespace

CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, const uint8_t * aad, size_t aad_length,
const Aes128BitsKeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
const Aes128KeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
uint8_t * tag, size_t tag_length)
{
VerifyOrReturnError(isBufferNonEmpty(nonce, nonce_length), CHIP_ERROR_INVALID_ARGUMENT);
Expand Down Expand Up @@ -123,7 +123,7 @@ CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, c
}

CHIP_ERROR AES_CCM_decrypt(const uint8_t * ciphertext, size_t ciphertext_length, const uint8_t * aad, size_t aad_length,
const uint8_t * tag, size_t tag_length, const Aes128BitsKeyHandle & key, const uint8_t * nonce,
const uint8_t * tag, size_t tag_length, const Aes128KeyHandle & key, const uint8_t * nonce,
size_t nonce_length, uint8_t * plaintext)
{
VerifyOrReturnError(isBufferNonEmpty(nonce, nonce_length), CHIP_ERROR_INVALID_ARGUMENT);
Expand Down Expand Up @@ -364,6 +364,21 @@ CHIP_ERROR HMAC_sha::HMAC_SHA256(const uint8_t * key, size_t key_length, const u
return CHIP_NO_ERROR;
}

CHIP_ERROR HMAC_sha::HMAC_SHA256(const Hmac128KeyHandle & key, const uint8_t * message, size_t message_length, uint8_t * out_buffer,
size_t out_length)
{
VerifyOrReturnError(isBufferNonEmpty(message, message_length), CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(out_buffer != nullptr && out_length == PSA_HASH_LENGTH(PSA_ALG_SHA_256), CHIP_ERROR_INVALID_ARGUMENT);

const psa_algorithm_t algorithm = PSA_ALG_HMAC(PSA_ALG_SHA_256);
psa_status_t status = PSA_SUCCESS;

status = psa_mac_compute(key.As<psa_key_id_t>(), algorithm, message, message_length, out_buffer, out_length, &out_length);
VerifyOrReturnError(status == PSA_SUCCESS, CHIP_ERROR_INTERNAL);

return CHIP_NO_ERROR;
}

CHIP_ERROR PBKDF2_sha256::pbkdf2_sha256(const uint8_t * pass, size_t pass_length, const uint8_t * salt, size_t salt_length,
unsigned int iteration_count, uint32_t key_length, uint8_t * key)
{
Expand Down
11 changes: 9 additions & 2 deletions src/crypto/CHIPCryptoPALmbedTLS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ static bool _isValidTagLength(size_t tag_length)
}

CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, const uint8_t * aad, size_t aad_length,
const Aes128BitsKeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
const Aes128KeyHandle & key, const uint8_t * nonce, size_t nonce_length, uint8_t * ciphertext,
uint8_t * tag, size_t tag_length)
{
CHIP_ERROR error = CHIP_NO_ERROR;
Expand Down Expand Up @@ -113,7 +113,7 @@ CHIP_ERROR AES_CCM_encrypt(const uint8_t * plaintext, size_t plaintext_length, c
}

CHIP_ERROR AES_CCM_decrypt(const uint8_t * ciphertext, size_t ciphertext_len, const uint8_t * aad, size_t aad_len,
const uint8_t * tag, size_t tag_length, const Aes128BitsKeyHandle & key, const uint8_t * nonce,
const uint8_t * tag, size_t tag_length, const Aes128KeyHandle & key, const uint8_t * nonce,
size_t nonce_length, uint8_t * plaintext)
{
CHIP_ERROR error = CHIP_NO_ERROR;
Expand Down Expand Up @@ -325,6 +325,13 @@ CHIP_ERROR HMAC_sha::HMAC_SHA256(const uint8_t * key, size_t key_length, const u
return CHIP_NO_ERROR;
}

CHIP_ERROR HMAC_sha::HMAC_SHA256(const Hmac128KeyHandle & key, const uint8_t * message, size_t message_length, uint8_t * out_buffer,
size_t out_length)
{
return HMAC_SHA256(key.As<Symmetric128BitsKeyByteArray>(), sizeof(Symmetric128BitsKeyByteArray), message, message_length,
out_buffer, out_length);
}

CHIP_ERROR PBKDF2_sha256::pbkdf2_sha256(const uint8_t * password, size_t plen, const uint8_t * salt, size_t slen,
unsigned int iteration_count, uint32_t key_length, uint8_t * output)
{
Expand Down
10 changes: 5 additions & 5 deletions src/crypto/PSASessionKeystore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class HmacKeyAttributes
HmacKeyAttributes()
{
psa_set_key_type(&mAttrs, PSA_KEY_TYPE_HMAC);
psa_set_key_algorithm(&mAttrs, PSA_ALG_HMAC(PSA_ALG_ANY_HASH));
psa_set_key_algorithm(&mAttrs, PSA_ALG_HMAC(PSA_ALG_SHA_256));
psa_set_key_usage_flags(&mAttrs, PSA_KEY_USAGE_SIGN_MESSAGE);
psa_set_key_bits(&mAttrs, CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES * 8);
}
Expand All @@ -68,7 +68,7 @@ class HmacKeyAttributes

} // namespace

CHIP_ERROR PSASessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Aes128BitsKeyHandle & key)
CHIP_ERROR PSASessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Aes128KeyHandle & key)
{
// Destroy the old key if already allocated
psa_destroy_key(key.As<psa_key_id_t>());
Expand All @@ -81,7 +81,7 @@ CHIP_ERROR PSASessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & ke
return CHIP_NO_ERROR;
}

CHIP_ERROR PSASessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Hmac128BitsKeyHandle & key)
CHIP_ERROR PSASessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Hmac128KeyHandle & key)
{
// Destroy the old key if already allocated
psa_destroy_key(key.As<psa_key_id_t>());
Expand All @@ -96,7 +96,7 @@ CHIP_ERROR PSASessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & ke
}

CHIP_ERROR PSASessionKeystore::DeriveKey(const P256ECDHDerivedSecret & secret, const ByteSpan & salt, const ByteSpan & info,
Aes128BitsKeyHandle & key)
Aes128KeyHandle & key)
{
PsaKdf kdf;
ReturnErrorOnFailure(kdf.Init(PSA_ALG_HKDF(PSA_ALG_SHA_256), secret.Span(), salt, info));
Expand All @@ -107,7 +107,7 @@ CHIP_ERROR PSASessionKeystore::DeriveKey(const P256ECDHDerivedSecret & secret, c
}

CHIP_ERROR PSASessionKeystore::DeriveSessionKeys(const ByteSpan & secret, const ByteSpan & salt, const ByteSpan & info,
Aes128BitsKeyHandle & i2rKey, Aes128BitsKeyHandle & r2iKey,
Aes128KeyHandle & i2rKey, Aes128KeyHandle & r2iKey,
AttestationChallenge & attestationChallenge)
{
PsaKdf kdf;
Expand Down
11 changes: 5 additions & 6 deletions src/crypto/PSASessionKeystore.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ namespace Crypto {
class PSASessionKeystore : public SessionKeystore
{
public:
CHIP_ERROR CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Aes128BitsKeyHandle & key) override;
CHIP_ERROR CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Hmac128BitsKeyHandle & key) override;
CHIP_ERROR CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Aes128KeyHandle & key) override;
CHIP_ERROR CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Hmac128KeyHandle & key) override;
CHIP_ERROR DeriveKey(const P256ECDHDerivedSecret & secret, const ByteSpan & salt, const ByteSpan & info,
Aes128BitsKeyHandle & key) override;
CHIP_ERROR DeriveSessionKeys(const ByteSpan & secret, const ByteSpan & salt, const ByteSpan & info,
Aes128BitsKeyHandle & i2rKey, Aes128BitsKeyHandle & r2iKey,
AttestationChallenge & attestationChallenge) override;
Aes128KeyHandle & key) override;
CHIP_ERROR DeriveSessionKeys(const ByteSpan & secret, const ByteSpan & salt, const ByteSpan & info, Aes128KeyHandle & i2rKey,
Aes128KeyHandle & r2iKey, AttestationChallenge & attestationChallenge) override;
void DestroyKey(Symmetric128BitsKeyHandle & key) override;
};

Expand Down
8 changes: 4 additions & 4 deletions src/crypto/RawKeySessionKeystore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ namespace Crypto {

using HKDF_sha_crypto = HKDF_sha;

CHIP_ERROR RawKeySessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Aes128BitsKeyHandle & key)
CHIP_ERROR RawKeySessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Aes128KeyHandle & key)
{
memcpy(key.AsMutable<Symmetric128BitsKeyByteArray>(), keyMaterial, sizeof(Symmetric128BitsKeyByteArray));
return CHIP_NO_ERROR;
}

CHIP_ERROR RawKeySessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Hmac128BitsKeyHandle & key)
CHIP_ERROR RawKeySessionKeystore::CreateKey(const Symmetric128BitsKeyByteArray & keyMaterial, Hmac128KeyHandle & key)
{
memcpy(key.AsMutable<Symmetric128BitsKeyByteArray>(), keyMaterial, sizeof(Symmetric128BitsKeyByteArray));
return CHIP_NO_ERROR;
}

CHIP_ERROR RawKeySessionKeystore::DeriveKey(const P256ECDHDerivedSecret & secret, const ByteSpan & salt, const ByteSpan & info,
Aes128BitsKeyHandle & key)
Aes128KeyHandle & key)
{
HKDF_sha_crypto hkdf;

Expand All @@ -46,7 +46,7 @@ CHIP_ERROR RawKeySessionKeystore::DeriveKey(const P256ECDHDerivedSecret & secret
}

CHIP_ERROR RawKeySessionKeystore::DeriveSessionKeys(const ByteSpan & secret, const ByteSpan & salt, const ByteSpan & info,
Aes128BitsKeyHandle & i2rKey, Aes128BitsKeyHandle & r2iKey,
Aes128KeyHandle & i2rKey, Aes128KeyHandle & r2iKey,
AttestationChallenge & attestationChallenge)
{
HKDF_sha_crypto hkdf;
Expand Down
Loading

0 comments on commit bdded49

Please sign in to comment.