Skip to content

Feat: Add NIST SP800-108 KDF mechanisms #257

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

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

Conversation

jacobprudhomme
Copy link
Contributor

@jacobprudhomme jacobprudhomme commented Apr 14, 2025

Author: Jacob Prud'homme
Email: [email protected]

Description

This PR adds support for the NIST SP800-108 KDF (aka KBKDF) in counter-, feedback- and double pipeline-modes

Summary of Changes

  • Added SP800_108_{COUNTER,FEEDBACK,DOUBLE_PIPELINE}_KDF mechanism types
    • Also, their corresponding Mechanism variants and associated parameters (KbkdfParams for counter- and double pipeline-mode, KbkdfFeedbackParams for feedback-mode)
    • Also, the types used within these parameter types (PrfDataParam, KbkdfDkmLengthFormat, KbkdfDkmLengthType, KbkdfCounterFormat, DerivedKey)
    • Also, tests for each mechanism (don't be afraid of the size of the diff! This is where the largest part of the changes comes from)
  • Updated warning on pointer casts in mechanism::make_mechanism()
  • Unrelated lint errors that popped up (not sure where they came from...)

Design Justifications

Since there's a decent amount of changes in this PR, I figured I'd start by justifying some of the choices I made, to preemptively answer any questions that might pop up.

  1. I didn't make KbkdfParams/KbkdfFeedbackParams transparent wrappers around the corresponding PKCS#11 struct, because it simply wasn't possible. Since we need to pass a &[Attribute] to the constructor for DerivedKey (unless we want to expose the end-user to the internal CK_ATTRIBUTE struct), and Attribute itself isn't a transparent wrapper around CK_ATTRIBUTE, we have to convert to a slice of CK_ATTRIBUTEs and store it internally, so that that contiguous section of memory stays valid for the needed duration. The consequences of this choice bubble up to the top level, and requires that the Kbkdf[Feedback]Params themselves can not be transparent for the same reason. Because of this, I had to add an accessor for the wrapped PKCS#11 type to get things to work with make_mechanism(). I have suggestions below about making this more flexible for the future
  2. I've settled on mutating the original DerivedKey structs the end-user creates to update them with the handle of the created key, and so it is up to that user to go back and collect these ObjectHandles for the additional derived keys after calling derive_key() (see this test for an example). While slightly less convenient, this more closely matches the paradigm of the spec itself (where pointers to handles are passed in via the params, and these are then mutated when the corresponding key is derived). The additional_derived_keys argument is also correspondingly marked mut, to signal that it will be mutated on the PKCS#11 backend
    1. As you can see in commit f1a15f7, my prior solution involved creating a new derive_keys() function, not present in the spec, as well as a HasAdditionalDerivedKeys trait to allow accessing such data on arbitrary param types. This clunky solution was significantly less attractive too
  3. I wrapped the internal boxed slices in DerivedKey and KbkdfParams/KbkdfFeedbackParams in a Pin. While pinning a box does nothing, I figured it was important to mark that this data should have a stable address, since the C side relies on pointers to it remaining valid

Questions/Feedback Wanted

General Questions

  • When looking at the HKDF implementation as an example, there are a bunch of accessors on HkdfParams that return different parts of the internal data. I'm unsure of their purpose, however. Should I also add these to Kbkdf[Feedback]Params?

Coding Style Questions

  • The Endianness enum is quite general, should I place this somewhere else?
  • The tests I wrote are quite big, I'd be happy to break them up if deemed necessary

Design Questions

  • I named all external-facing types as Kbkdf(...) instead of something like Sp800108(...), because the naming of the latter is atrocious. However, the term KBKDF is never used in the spec and may lead to people not easily discovering the functionality they need. How do you feel about this naming?
  • As you can see in commit 8ac48ed, I previously had an implementation with separate PrfCounterDataParam and PrfDataParam types, as there are some differences between the data parameters allowed by the KDF in counter-mode vs the other modes. Notably, in counter-mode, IterationVariable must take a KbkdfCounterFormat argument, and the Counter data parameter type is not allowed. While distinguishing these two types of params ensures the end-user could not make mistakes when configuring the KDF, it goes against the spec, which has just a single CK_PRF_DATA_PARAM type. Would the type-safety focus of the original design be preferred over the current 1-to-1 mapping with the spec? In the latter case, the backend should return CKR_MECHANISM_PARAM_INVALID for these types of errors anyway, but it may be nice to stop these errors at the type system-level

Suggestions for Future

Both of these came up while trying to work around the limitations I ran into on Design Justifications (1).

  1. What if we made Attribute a transparent wrapper around CK_ATTRIBUTE? That way we could get rid of the inconsistencies between constructing a Kbkdf[Feedback]Params vs other parameter types, i.e. have a tree of just transparent wrappers all the way to the root params object, which we can directly pass as a pointer to the C side, as was done up until now
  2. Solving part of the same issue as above, what if we made some sort of trait AsPkcsParams defining a as_pkcs_params_ptr() method with the following default implementation that covers 99% of the cases so far (i.e. all cases except this one):
fn as_pkcs_params_ptr(&self) -> *mut c_void {
    self as *const T as *mut c_void
}

While this partially overlaps with (1) in terms of solving some inconsistencies that I've had to introduce to make my changes fit, I think it has other benefits. While not so important because make_mechanism() is internal, it would still prevent developers from making a mistake and enforce that the function is only used with parameter objects that explicitly implement the trait, instead of arbitrary types. As well, the impact of implementing this is quite small. I could even make that a part of this PR, if you're interested!

@jacobprudhomme jacobprudhomme marked this pull request as ready for review April 14, 2025 15:01
…locations in PKCS#11 structs

Signed-off-by: Jacob Prud'homme <[email protected]>
…correctly

Because I was forgetting that the slice of Attributes within had to be converted
to a slice of CK_ATTRIBUTEs! This need to convert internally (otherwise exposing
end users to the internal CK_ATTRIBUTE type) meant that we needed to store this
converted slice within each DerivedKey, so that the memory stayed valid. This
meant playing around with a few changes to the standard "PKCS#11 struct wrapped
in a Rust struct marked `transparent`" pattern that had been used for params

Signed-off-by: Jacob Prud'homme <[email protected]>
Gave up some type safety on which types of PRF data parameters are allowed for
which KDF modes, in exchange for being a 1-to-1 binding with the PKCS#11 spec.
Depending on the wants of the project, this can be reversed.

Signed-off-by: Jacob Prud'homme <[email protected]>
Signed-off-by: Jacob Prud'homme <[email protected]>
Changed my mind on coding style

Signed-off-by: Jacob Prud'homme <[email protected]>
Signed-off-by: Jacob Prud'homme <[email protected]>
Signed-off-by: Jacob Prud'homme <[email protected]>
Signed-off-by: Jacob Prud'homme <[email protected]>
wiktor-k
wiktor-k previously approved these changes Apr 22, 2025
Copy link
Collaborator

@wiktor-k wiktor-k left a comment

Choose a reason for hiding this comment

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

I think it looks good. It's quite a sizable change 😅

The make_mechanism made me think of #225 🤔

/// * `endianness` - The endianness of the counter's bit representation.
///
/// * `width_in_bits` - The number of bits used to represent the counter value.
pub fn new(endianness: Endianness, width_in_bits: usize) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does width_in_bits have any value constraints? Maybe using NonZeroUsize would be a good idea? 🤔

Copy link
Contributor Author

@jacobprudhomme jacobprudhomme Apr 22, 2025

Choose a reason for hiding this comment

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

The spec doesn't explicitly say anything: https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/os/pkcs11-curr-v3.0-os.html#_Toc30061446. But I think it's logical that if you didn't want a Counter or DkmLength format object to be present in the input data, you would just leave it out instead of providing one with length 0. I'll make this change

/// Container for information on an additional key to be derived.
#[derive(Debug)]
pub struct DerivedKey {
template: Pin<Box<[CK_ATTRIBUTE]>>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just out of curiosity why this is pinned?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Essentially I just wanted to include some visual hint to the programmer that it's crucial that the address of this data doesn't change (because of the fact that we pass pointers into it to the backend). I know this is redundant since Box guarantees this anyway, but I thought it would be nice to make this explicit. If you think it's just noise or could be confusing, I'm happy to remove it

#[derive(Debug)]
pub struct KbkdfParams<'a> {
/// Holds own data so that we have a contiguous memory region to give to backend
_additional_derived_keys: Option<Pin<Box<[CK_DERIVED_KEY]>>>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why the underscore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we don't use this member, the lint check complains without it. It's just an internal member needed to hold the slice of additional key PKCS#11 structs so that it remains valid for the lifetime of the params object (this is a consequence of what I explained in the Design Justifications section of the PR body)


/* DOUBLE PIPELINE-MODE */

/* FIXME: NIST SP800-108 in double-pipeline mode is not yet supported by SoftHSM or Kryoptic */
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be good to move all these unsupported cases into a separate test and mark them with #[ignore = "unsupported by both softhsm and kryoptic"].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

jacobprudhomme and others added 4 commits April 22, 2025 18:10
Co-authored-by: Wiktor Kwapisiewicz <[email protected]>
Signed-off-by: Jacob Prud'homme <[email protected]>
…nd `DkmLengthFormat`

Signed-off-by: Jacob Prud'homme <[email protected]>
…o indicate necessity of mutability

Signed-off-by: Jacob Prud'homme <[email protected]>
@jacobprudhomme
Copy link
Contributor Author

jacobprudhomme commented Apr 22, 2025

@wiktor-k glad to know I was on the same wavelength as previous work using mutable parameters! I was worried I was doing something weird.

Also, did you have any comments/remarks on what I've written in the PR body?

@wiktor-k
Copy link
Collaborator

Also, did you have any comments/remarks on what I've written in the PR body?

Tbh I want to calmly re-read it as I didn't have much time for open-source reviews today. I'll add a more concrete comment soon.

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.

2 participants