Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ML-KEM and ML-KEM hybrid #103

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

ML-KEM and ML-KEM hybrid #103

wants to merge 22 commits into from

Conversation

mamckee
Copy link
Collaborator

@mamckee mamckee commented Dec 30, 2024

This PR adds support for ML-KEM and hybrid ECDHE-MLKEM to the SymCrypt provider. This PR also refactors a significant portion of ECC key management and exposes the ECDH provider interface for ML-KEM hybrid operations. This implementation has been tested against BoringSSL and the OQS provider for compatibility.

These algorithms are not currently present in the default OpenSSL implementation. This PR adds the encoder and decoder interface, key management, and KEM interfaces for ML-KEM and ML-KEM hybrid. Their behavior is subject to change to align with the default OpenSSL behavior when OpenSSL adds their own implementation.

Code points are taken from IETF drafts and are subject to change. These are the same code points used by BoringSSL and the OQS provider. ML-KEM hybrid is implemented according to the latest IETF draft but is subject to changes.

The following algorithms are added:

Algorithm Code point OID
mlkem512 0x0200 2.16.840.1.101.3.4.4.1
mlkem768 0x0201 2.16.840.1.101.3.4.4.2
mlkem1024 0x0202 2.16.840.1.101.3.4.4.3
secp256r1mlkem768 0x11eb 2.16.840.1.101.3.4.4.4 (placeholder)
x25519mlkem768 0x11ec 2.16.840.1.101.3.4.4.5 (placeholder)
secp384r1mlkem1024 0x11ed 2.16.840.1.101.3.4.4.6 (placeholder)

draft-connolly-tls-mlkem-key-agreement (ML-KEM standalone usage and code points)
draft-kwiatkowski-tls-ecdhe-mlkem (ML-KEM hybrid implementation and code points)

TLS_GROUP_ENTRY("mlkem768", SCOSSL_SN_MLKEM768, "MLKEM", scossl_tls_group_info_mlkem768),
TLS_GROUP_ENTRY("mlkem1024", SCOSSL_SN_MLKEM1024, "MLKEM", scossl_tls_group_info_mlkem1024),
TLS_GROUP_ENTRY("secp256r1mlkem768", SCOSSL_SN_P256_MLKEM768, "MLKEM", scossl_tls_group_info_secp256r1mlkem768),
TLS_GROUP_ENTRY("P256mlkem768", SCOSSL_SN_P256_MLKEM768, "MLKEM", scossl_tls_group_info_secp256r1mlkem768),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P256

Just curious if this is definitely names without the "-" in P256?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These strings are only for setting supported groups from configuration as far as I'm aware. Now that OpenSSL has an ML-KEM implementation I'll update these entries to match what they have: https://github.com/openssl/openssl/blob/c2ab75e30a211aa278f8da1f0f040f9368adb81d/providers/common/capabilities.c#L193-L194

@samuel-lee-msft
Copy link
Contributor

samuel-lee-msft commented Feb 26, 2025

static const OSSL_PARAM p_scossl_supported_group_list[][11] = {

Not sure why this was 11 before - it looks like TLS_GROUP_ENTRY previously defined 10 OSSL_PARAMs including the OSSL_PARAM_END, and only now defines 11.

Seems error prone to have the magic number.


Refers to: SymCryptProvider/src/p_scossl_base.c:186 in 3b662e7. [](commit_id = 3b662e7, deletion_comment = False)

if (curve == NULL)
{
return 0;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _In_ annotation on this function indicates that curve is a required parameter which must not be NULL. In Windows and SymCrypt, we'd typically not do runtime parameter validation here, but would instead rely on assertions and/or PREfast to catch invalid calls.

That said, if this is public function, it probably makes sense to be more defensive. I'm also not sure what the OpenSSL philosophy is regarding input validation.

Copy link
Collaborator Author

@mamckee mamckee Mar 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. This is an internal function so I can ensure we do the validation outside of this function. I don't think this needs to be changed to _In_opt_

//

// Digests
#define SCOSSL_ALG_NAME_MD5 SN_md5"SSL3-MD5:1.2.840.113549.2.5"
Copy link
Contributor

@samuel-lee-msft samuel-lee-msft Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"

Do we not need ":" separators here? (same for all the new definitions of digest ALG_NAMEs other than CSHAKE)


// KDF
#define SCOSSL_ALG_NAME_HKDF SN_hkdf
#define SCOSSL_ALG_NAME_KBKKDF "KBKDF"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KBKKDF

NP: KBKDF

@@ -586,23 +729,15 @@ SCOSSL_STATUS OSSL_provider_init(_In_ const OSSL_CORE_HANDLE *handle,
{
SYMCRYPT_MODULE_INIT();
if (!scossl_dh_init_static() ||
!scossl_ecc_init_static())
!scossl_ecc_init_static() ||
!p_scossl_register_extended_algorithms())
{
ERR_raise(ERR_LIB_PROV, ERR_R_INIT_FAIL);
Copy link
Contributor

@samuel-lee-msft samuel-lee-msft Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ERR_raise(ERR_LIB_PROV, ERR_R_INIT_FAIL);

Leaks p_ctx, p_ctx->coreBioMeth, and static dlgroups / ecurves.
Probably should call p_scossl_teardown. Consider refactoring away from early returns to cleanup section style.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed in #104. I'll rebase after the current round of comments.

}

p_ctx->handle = handle;
p_ctx->libctx = OSSL_LIB_CTX_new_child(handle, in);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p_ctx->libctx

Q: should this be explicitly freed in p_scossl_teardown?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also addressed in #104 and will be updated with a rebase.

{
for (SIZE_T i = 0; i < sizeof(p_scossl_mlkem_groups) / sizeof(SCOSSL_MLKEM_GROUP_INFO); i++)
{
p_scossl_mlkem_groups[i].nid = OBJ_create(p_scossl_mlkem_groups[i].oid, p_scossl_mlkem_groups[i].groupName, p_scossl_mlkem_groups[i].groupName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p_scossl_mlkem_groups[i].groupName

NP: Is it correct to pass the shortname into this longname parameter, or should we leave it as NULL?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK for now since it's only used internally, but it should be updated to match the incoming OpenSSL implementation: https://github.com/openssl/openssl/blob/c2ab75e30a211aa278f8da1f0f040f9368adb81d/include/openssl/obj_mac.h#L6575

size_t *bytes_read)
{
if (core_bio_read_ex == NULL)
return 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0

NP: SCOSSL_FAILURE / SCOSSL_SUCCESS for returns in this file?

Copy link
Collaborator Author

@mamckee mamckee Mar 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just required for supporting encoder/decoder so I probably won't bother with annotating parameters but I think that makes sense.

p_scossl_bio_core_gets, p_scossl_bio_core_puts, and p_scossl_bio_core_ctrl should keep their current return types as their return isn't success/fail.

switch (dispatch->function_id)
{
case OSSL_FUNC_BIO_READ_EX:
core_bio_read_ex = (OSSL_FUNC_BIO_read_ex_fn *)dispatch->function;
Copy link
Contributor

@samuel-lee-msft samuel-lee-msft Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(OSSL_FUNC_BIO_read_ex_fn *)dispatch->function

NP: OSSL_FUNC_BIO_read_ex(dispatch)?

}

BIO_free(bio);
ctx->desc->freeKeyCtx(keyCtx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NP: Probably doesn't matter, but probably should free keyCtx before bio?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really matter but it does look cleaner.


typedef struct scossl_decode_ctx_st
{
SCOSSL_PROVCTX *provctx;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCOSSL_PROVCTX *provctx;

It seems like we currently have a recipe for potential use-after-free with the number of provider structures keeping a pointer to the single provider context associated with a OSSL_provider_init call. (i.e. can we ever legitimately have a situation where p_scossl_teardown is called while p_scossl_decode is in progress?)

Should we have a refcount-ed provctx, or is OpenSSL guaranteed to ensure that all provider structures are freed before the provider is torn down? Perhaps it's not a huge deal if SCOSSL is the default provider, but could definitely be more of an issue for a place where an application dynamically loads/unloads SCOSSL.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a legitimate case. The OpenSSL default provider doesn't refcount this so we should be ok.

It seems like the providers were originally built with that being a valid use case (which is why we saw use-after-free issues in openssl speed), but it seems like the assumption now is that no provider operations should ever take place after p_scossl_teardown

return NULL;
}

if ((bio = BIO_new(provctx->coreBioMeth)) != NULL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BIO_new(provctx->coreBioMeth)

Should we use:
BIO_new_ex(provctx->libctx, provctx->coreBioMeth)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's probably better.

pbPrivateKey, cbPrivateKey,
NULL, 0,
numFormat,
pointFormat,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointFormat

NP: Technically this is a don't care value when cbPublicKey is 0 (the private key is just a number, not a point)

copyCtx->isX25519 = keyCtx->isX25519;
copyCtx->libctx = keyCtx->libctx;
copyCtx->modifiedPrivateBits = keyCtx->modifiedPrivateBits;
copyCtx->conversionFormat = keyCtx->conversionFormat;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge NP: this should probably only be copied when copying private key material

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a weird edge case where a key's conversion format is set, the key is duplicated, and the private key is imported later.

I'm not sure if that's even a valid case but I don't think it hurts to just copy the value since it technically falls under OSSL_KEYMGMT_SELECT_OTHER_PARAMETERS

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I misclicked when leaving the comment - I meant this to refer to the modifiedPrivateBits field

if (copyCtx != NULL)
{
#ifdef KEYSINUSE_ENABLED
copyCtx->keysinuseLock = CRYPTO_THREAD_lock_new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should copy isImported in here too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is missing in RSA too.

@mamckee
Copy link
Collaborator Author

mamckee commented Mar 1, 2025

static const OSSL_PARAM p_scossl_supported_group_list[][11] = {

Yeah that was easy to miss... I'll pull that out into a constant defined by the TLS_GROUP_ENTRY definition.


In reply to: 2686350183


Refers to: SymCryptProvider/src/p_scossl_base.c:186 in 3b662e7. [](commit_id = 3b662e7, deletion_comment = False)

return SCOSSL_SUCCESS;
}

SIZE_T p_scossl_ecc_get_max_size(_In_ SCOSSL_ECC_KEY_CTX *keyCtx, BOOL isEcdh)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p_scossl_ecc_get_max_size

NP: name to indicate this is the size of the result of an ECC operation? I was a bit perplexed by what this function was for.

{
return SymCryptEcurveSizeofScalarMultiplier(keyCtx->curve);
}
else if (keyCtx->isX25519)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else

Why else?

}

cbKey = SymCryptEcurveSizeofFieldElement(keyCtx->curve);
if (!keyCtx->isX25519)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!keyCtx->isX25519)

Isn't this always true given the above code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In p_scossl_ecc_get_encoded_public_key we compute the same thing with:

pointFormat = keyCtx->conversionFormat == POINT_CONVERSION_COMPRESSED ? SYMCRYPT_ECPOINT_FORMAT_X : SYMCRYPT_ECPOINT_FORMAT_XY;

// Allocate one extra byte for point compression type
cbPublicKey = SymCryptEckeySizeofPublicKey(keyCtx->key, pointFormat) + 1;

Which I think is probably a bit clearer

{
SYMCRYPT_NUMBER_FORMAT numFormat;
SYMCRYPT_ECPOINT_FORMAT pointFormat;
PBYTE pbPublicKey, pbPublicKeyStart;
PBYTE pbPublicKeyStart = NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pbPublicKeyStart

Just for tracking the logic here, suggest minor renaming:
pbPublicKeyStart -> pbEncodedPublicKey
pbPublicKey -> pbRawPublicKey

And then track cbEncodedPublicKey and cbRawPublicKey?
Tweaking cbPublicKey with ++s and --s seems brittle if new functionality is ever added in here.

ret = SCOSSL_SUCCESS;

cleanup:
if (!ret && allocatedKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!ret

NP: ret != SCOSSL_SUCCESS

if (keyCtx->key != NULL)
{
SymCryptEckeyFree(keyCtx->key);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NP: Prefer to keep SymCryptEckeyFree(keyCtx->key); right next to the SymCryptEckeyAllocate(keyCtx->curve), or set keyCtx->key to NULL after free.
Want to make it harder to introduce a double free in future (i.e. if adding some new parameter validation which goto cleanup between Free and Allocate).


// Preserve original bits for export
keyCtx->modifiedPrivateBits = pbPrivateKey[0] & 0x07;
keyCtx->modifiedPrivateBits |= pbPrivateKey[cbPrivateKey-1] & 0xc0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cbPrivateKey

cbPrivateKey is still 0 here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually looks like there is no need for cbPrivateKey at all, and you can just use cbEncodedPrivateKey everywhere instead in this function?

// to copy and decode the public key.
if (keyCtx->isX25519)
{
OPENSSL_secure_clear_free(pbPrivateKey, cbPrivateKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cbPrivateKey

This should be cbEncodedPrivateKey

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants