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

eXtendable Output Functions as first-class citizen #3671

Merged
merged 5 commits into from
Sep 14, 2023

Conversation

reneme
Copy link
Collaborator

@reneme reneme commented Aug 15, 2023

Motivation

Most PQC schemes use XOFs as a building block in one or the other way. This is a proposal to establish XOF as a new public API base class. The only implementation currently provided is SHAKE-128 and SHAKE-256.

Pull Request Dependencies

Find included

  • New top-level module "xof" with class XOF as abstract base class
    • start() allows priming certain XOFs with a salt and key (otherwise its a NOOP)
    • update() allows streamed input of an arbitrarily long message
    • output() allows streamed access to the XOF's infinitely long output stream
    • typical implementations won't allow update()s after the first call to output()
  • SHAKE_128_XOF and SHAKE_256_XOF as sub-classes of XOF
  • cSHAKE_128_XOF and cSHAKE_256_XOF as sub-classes of XOF (for internal use only)
  • internal helper functions (roughly) as defined in NIST SP.800-185 Section 2.3
    (those functions can be reused to implement KMAC -- see KMAC, 2nd: added keccak-fips as standalone; added KMAC-256 #3570)

Things to be considered

XOFs based on stream ciphers

Kyber and Dilithium build their "90s-crypto" variants on XOF constructions based on AES-256/CTR. We could look into providing support for such stream-cipher XOFs independent of the CRYSTALS algorithms. (See #3672)

Retrofitting PQC algorithms

That's future work and will be done in a follow-up. (See #3672)

@reneme reneme added the enhancement Enhancement or new feature label Aug 15, 2023
@reneme reneme added this to the Botan 3.2.0 milestone Aug 15, 2023
@reneme reneme self-assigned this Aug 15, 2023
@reneme reneme force-pushed the feature/xof-interface branch from e0adc08 to 291c56f Compare August 15, 2023 16:04
@coveralls
Copy link

coveralls commented Aug 15, 2023

Coverage Status

coverage: 91.667% (-0.04%) from 91.707% when pulling afb70af on Rohde-Schwarz:feature/xof-interface into 28b5423 on randombit:master.

@reneme reneme force-pushed the feature/xof-interface branch 2 times, most recently from b28bd76 to afa8bae Compare August 16, 2023 05:20
@reneme reneme marked this pull request as ready for review August 16, 2023 05:35
@reneme reneme force-pushed the feature/xof-interface branch from afa8bae to 7f1b216 Compare August 16, 2023 10:47
@reneme reneme force-pushed the feature/xof-interface branch from 7f1b216 to 0a0291d Compare August 17, 2023 08:49
@reneme
Copy link
Collaborator Author

reneme commented Aug 17, 2023

Rebased onto @falko-strenzke's Keccak refactoring as a sanity checks. Findings are in #3673.

src/lib/base/buf_input.h Outdated Show resolved Hide resolved
src/lib/hash/shake/shake.h Outdated Show resolved Hide resolved
src/lib/stream/shake_cipher/shake_cipher.h Outdated Show resolved Hide resolved
@reneme reneme force-pushed the feature/xof-interface branch from 0a0291d to 75ec144 Compare August 18, 2023 13:26
@reneme
Copy link
Collaborator Author

reneme commented Aug 18, 2023

The whole Keccak-Refactoring is becoming a mess with several inter-dependent pull requests. Sorry for that. Lets put this one on-hold until #3673 and #3675 are integrated.

Edit: done

@reneme reneme force-pushed the feature/xof-interface branch from 75ec144 to b3daafb Compare August 19, 2023 11:20
@reneme reneme force-pushed the feature/xof-interface branch 2 times, most recently from 0276fac to ba45416 Compare August 22, 2023 07:20
@reneme
Copy link
Collaborator Author

reneme commented Aug 22, 2023

Re: XOFs based on stream ciphers

Maybe we should step back and rethink the XOF interface somewhat. Looking at NIST SP.800-185 they define things like KMAC-XOF that is KMAC with a variable output length. I.e. just like the AES/CTR-XOF used in CRYSTALS Kyber/Dilithium, it takes a key (and potentially other customization parameters).

Another aspect is cSHAKE (also NIST SP.800-185), which is basically SHAKE, but with additional domain separation parameters N and S. See here for further discussion.

I think it would be great to incorporate such XOFs off the bat, when introducing a new API.

@reneme
Copy link
Collaborator Author

reneme commented Aug 22, 2023

To my mind an abstract XOF should have two phases:

  1. Parameterization
  2. Output

The "output phase" is simple and inherently generic. The user just calls ::output() as often as they want to read the indefinite stream of XOF output bytes.

The "parameterization phase" is algorithm-specific, unfortunately. Here are some relevant examples:

Algorithm Parameters
SHAKE an arbitrary-length message msg
cSHAKE arbitrary-lengths N and S, and msg
AES/CTR-XOF a short iv and key
KMAC-XOF arbitrary-length key, msg and salt

Reflecting this parameter space in a common XOF base-interface is possible but probably won't result in a convenient API. Except, if the only "common" feature of this interface is the ::output() method which could be shared by all XOFs.

In fact, looking at the existing use cases in Kyber and Dilithium, this is exactly what we need there. Depending on the algorithm mode ("90s" or "modern"), the AES/CTR or SHAKE XOF is parameterized inside a polymorphic method and then handed out as an abstract (pre-parameterized) reference. The common code, that is independent of the specific mode, then just "reads" from the abstract XOF object via ::output().

@reneme
Copy link
Collaborator Author

reneme commented Aug 22, 2023

Given the above-described constraints, it seems reasonable to abandon the ubiquitous ::create("AlgoSpec") factory methods for XOFs. Instead, the user could instantiate the concrete XOF subclass, parameterize it and then (maybe) use it via the abstract XOF base class that just provides the ::output() functionality.

@randombit Does that sound like a reasonable abstraction to you?

@reneme reneme mentioned this pull request Aug 23, 2023
@reneme
Copy link
Collaborator Author

reneme commented Aug 24, 2023

Benchmarking the SHAKE-128/256 XOF showed comparable results for update() and output() and correspond to the measured performance of SHA-3(256). No obvious bottlenecks.

absorb() squeeze()
SHAKE-128 555MiB/sec 530MiB/sec
SHAKE-256 448MiB/sec 432MiB/sec

@randombit
Copy link
Owner

Instead, the user could instantiate the concrete XOF subclass, parameterize it and then (maybe) use it via the abstract XOF base class that just provides the ::output() functionality.

I very much dislike this. A lot of effort was spent in 2->3 in removing all of the concrete subclasses from the public API; this minimizes API and ABI surface. This would also complicate the FFI interface.

I don't think CTR mode "XOF" is worth optimizing for. It is a weird corner case that should not be exposed to applications. And it seems Kyber is dropping "90s mode" entirely (#3543) so it is probably going to be removed soonish anyway.

Then we're left with

  • SHAKE: arbitrary length msg
  • cSHAKE: arbitrary length msg. N is not for us - quoting SP 800-185 "N is a function-name bit string, used by NIST to define functions based on cSHAKE. When no function other than cSHAKE is desired, N is set to the empty string." and so can be ignored. And S is a domain separator and can be instantiated in the usual way eg cSHAKE(256, my_domain_dep)
  • KMAC-XOF: arbitrary length msg, key, and salt.
class XOF {
  virtual void start(std::span<const uint8_t> salt = {}, std::span<uint8_t> key = {}) = 0;
  virtual bool valid_salt_length(size_t salt_len) = 0;
  // returns Key_Length_Specification(0) if key is not supported
  virtual Key_Length_Specification key_spec() const = 0;
  virtual void update(std::span<const uint8_t> input);
  //...
};

CTR, which doesn't support inputs, is handled by the funny case that XOF::update throws Not_Implemented.

@falko-strenzke
Copy link
Collaborator

  • cSHAKE: arbitrary length msg. N is not for us - quoting SP 800-185 "N is a function-name bit string, used by NIST to define functions based on cSHAKE. When no function other than cSHAKE is desired, N is set to the empty string." and so can be ignored. And S is a domain separator and can be instantiated in the usual way eg cSHAKE(256, my_domain_dep)

KMAC needs to set N and S, see the KMAC spec

@reneme
Copy link
Collaborator Author

reneme commented Aug 24, 2023

@randombit Thanks for your opinion. Your points make sense and I agree in general. I'll be AFK until the beginning of September and will come back to this after.

Copy link
Owner

@randombit randombit left a comment

Choose a reason for hiding this comment

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

First pass review

virtual Key_Length_Specification key_spec() const = 0;

/**
* @return the intrinsic processing block size of this XOF
Copy link
Owner

Choose a reason for hiding this comment

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

This should probably also mention it returns 0 if there is no such value.

What is this used for?

Copy link
Owner

Choose a reason for hiding this comment

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

If std::optional wasn't so awkward to use I'd suggest returning an optional here. :/

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 changed this to be a pure-virtual, forcing downstream implementations to make up their minds.

}

void SHAKE_XOF::start(std::span<const uint8_t> salt, std::span<const uint8_t> key) {
BOTAN_UNUSED(key);
Copy link
Owner

Choose a reason for hiding this comment

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

If the caller provides a non-empty key here it is just ignored, which is going to be confusing and a possible security issue if someone misuses the API. We should instead throw Invalid_Key_Length if this occurs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. Now implemented as the base-implementation of XOF::start(). Also the base methods XOF::key_spec() and XOF::valid_salt_length() now have default implementations that basically disable support for keys and salts by default.

src/lib/xof/shake_xof/shake_xof.cpp Outdated Show resolved Hide resolved
src/lib/xof/xof.h Outdated Show resolved Hide resolved
src/tests/test_xof.cpp Show resolved Hide resolved
// We deliberately do not check the salt length here. There's no technical
// reason to reject a salt buffer passed in by an application that exceeds
// the (arbitrarily set) valid_salt_length() restriction.
keccak_absorb_padded_strings_encoding(*this, block_size(), m_function_name, salt);
Copy link
Owner

Choose a reason for hiding this comment

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

Not restricting the cSHAKE salts seems fine, but we also here will accept a salt with plain SHAKE XOF where this is not defined at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yikes, indeed! The condition above was previously meant to exclude non-cSHAKE instances of this base class. But I must have broken it in some restructuring. Anyway, that's certainly not optimal.

I'll look into giving the cSHAKE variants their own base class and leave the salt logic out of the vanilla SHAKE implementation.

@reneme reneme force-pushed the feature/xof-interface branch 3 times, most recently from 72afe40 to 32f5dc4 Compare September 11, 2023 08:49
@reneme
Copy link
Collaborator Author

reneme commented Sep 11, 2023

Thanks for the review! I rebased to latest master (after merging the std::span refactorings) and addressed your review comments.

Most notably, I split the SHAKE_XOF and cSHAKE_XOF into two separate base classes (and modules). That results in a handful duplicated lines (::add_data(), ::generate_bytes()) but splits off the salt handling in ::start() much more logically.

Also, I XOF::start() is no virtual method anymore. Instead, it implements the required sanity checks for salt and key and then calls the private-virtual start_msg() method. Subclasses may or may not implement start_msg().

@reneme reneme force-pushed the feature/xof-interface branch from 32f5dc4 to afb70af Compare September 11, 2023 09:39
Copy link
Owner

@randombit randombit left a comment

Choose a reason for hiding this comment

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

Looks good now. Should we have some tests for cSHAKE as well?

@falko-strenzke
Copy link
Collaborator

falko-strenzke commented Sep 13, 2023

There are four test vectors from NIST, all of which have an empty value for N.

I just implemented cSHAKE for another libgcrpyt and created two test vectors with non empty N using https://asecuritysite.com/golang/cs. I checked their results for empty N and that was at least fine. In any case, the KMAC test vectors will also verify non-empty N.

  { /* Created with https://asecuritysite.com/golang/cs */
    GCRY_MD_CSHAKE128,
    "00010203",
    "ABC",
    "Email Signature",
    32,
    "5CF74DC523ADC0B97EC3614E703835277E9F818879AA1EAE5B2B4E4472EB6A68" },
  { /* Created with https://asecuritysite.com/golang/cs */
   CSHAKE256,
    "00010203", // data hex
    "ABC", // N as C string
    "Email Signature", // S as C string
    32, // output size in bytes
    "0C34C14C4A56E5FC01BE8C04C759DA61437E86B88DF3E21A934436D427A85E9D"  // expected output hex
}, 

@randombit
Copy link
Owner

Thanks for checking this @falko-strenzke. @reneme can we get these included here so we have some baseline check on cSHAKE? I guess it is a little complicated by cSHAKE not being exposed to applications so you can't immediately use the normal XOF test.

@reneme reneme force-pushed the feature/xof-interface branch 2 times, most recently from f52a8aa to 14b4183 Compare September 13, 2023 16:57
@reneme
Copy link
Collaborator Author

reneme commented Sep 13, 2023

@falko-strenzke Nice! Thanks for the test vectors. I did look for something "official" for cSHAKE but wasn't successful.

@randombit I hacked the XOF allocation in test_xof.cpp so that it explicitly deals with the internal cSHAKE XOFs when needed. Similarly, I also added vectors for the AES/CTR-XOF in #3672 a while ago.

@randombit
Copy link
Owner

@reneme looks good thanks. I couldn't find the tests themselves though - are you missing a git add of cshake.vec?

@reneme reneme force-pushed the feature/xof-interface branch from 14b4183 to 73f7c2d Compare September 13, 2023 17:10
@reneme reneme force-pushed the feature/xof-interface branch from 73f7c2d to e499bbf Compare September 13, 2023 17:11
@reneme
Copy link
Collaborator Author

reneme commented Sep 13, 2023

I couldn't find the tests themselves though - are you missing a git add of cshake.vec?

🤦‍♂️

@reneme
Copy link
Collaborator Author

reneme commented Sep 13, 2023

I was fighting some linker visibility issues. Had to move the cSHAKE_XOF::provider() and ::block_size() implementations into the *.cpp file to avoid exposing the Keccak_Permutation in the shared library.

Now everything should turn green. ☺️

@reneme
Copy link
Collaborator Author

reneme commented Sep 14, 2023

🥳

@reneme reneme merged commit 43dcc8a into randombit:master Sep 14, 2023
@reneme reneme deleted the feature/xof-interface branch September 14, 2023 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Enhancement or new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants