From bd4fc17fdca7ca6dfdc3d83f7f71d8a773f79d86 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 4 Apr 2025 18:32:14 +0900 Subject: [PATCH 1/2] digest: add OpenSSL::Digest#squeeze Add support for extendable-output functions (XOFs). An XOF is a hash function that can produce arbitrary length of output. The interface of the existing Digest::Class#digest and its variants is unsuitable for this purpose. Add a new method to "squeeze" the specified amount of output from the XOF. This method can be called repeatedly. OpenSSL 3.3 or later and AWS-LC support this. --- ext/openssl/extconf.rb | 3 +++ ext/openssl/ossl_digest.c | 37 +++++++++++++++++++++++++++++++++++++ test/openssl/test_digest.rb | 18 ++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 6c7417899..68cbd9fe1 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -169,6 +169,9 @@ def find_openssl_library have_func("SSL_get0_group_name(NULL)", ssl_h) have_func("OSSL_HPKE_CTX_new(0, (OSSL_HPKE_SUITE){0}, 0, NULL, NULL)", "openssl/hpke.h") +# added in 3.3.0 +have_func("EVP_DigestSqueeze(NULL, NULL, 0)", evp_h) + # added in 3.4.0 have_func("TS_VERIFY_CTX_set0_certs(NULL, NULL)", ts_h) diff --git a/ext/openssl/ossl_digest.c b/ext/openssl/ossl_digest.c index e23968b1e..8223e7d3a 100644 --- a/ext/openssl/ossl_digest.c +++ b/ext/openssl/ossl_digest.c @@ -292,6 +292,42 @@ ossl_digest_finish(VALUE self) return str; } +#ifdef HAVE_EVP_DIGESTSQUEEZE +/* + * call-seq: + * digest.squeeze(length) -> string + * + * Gets the next _length_ bytes of output from the extendable-output function + * (XOF). This method can be called multiple times to get more output. + * + * Unlike #digest, this method mutates the digest instance. Once called, no + * further #update or #<< calls are allowed. Use #dup to create a copy before + * calling this method if you need intermediate digest values. + * + * See also the man page EVP_DigestSqueeze(3). + * This method is compatible with OpenSSL 3.3 or later. + */ +static VALUE +ossl_digest_squeeze(VALUE self, VALUE length) +{ + EVP_MD_CTX *ctx; + VALUE str; + size_t size; + + GetDigest(self, ctx); + size = NUM2SIZET(length); + if (size > LONG_MAX) + rb_raise(rb_eArgError, "length is negative or too big"); + str = rb_str_new(NULL, (long)size); + if (!EVP_DigestSqueeze(ctx, (unsigned char *)RSTRING_PTR(str), size)) + ossl_raise(eDigestError, "EVP_DigestSqueeze"); + + return str; +} +#else +#define ossl_digest_squeeze rb_f_notimplement +#endif + /* * call-seq: * digest.name -> string @@ -466,6 +502,7 @@ Init_ossl_digest(void) rb_define_method(cDigest, "update", ossl_digest_update, 1); rb_define_alias(cDigest, "<<", "update"); rb_define_private_method(cDigest, "finish", ossl_digest_finish, 0); + rb_define_method(cDigest, "squeeze", ossl_digest_squeeze, 1); rb_define_method(cDigest, "digest_length", ossl_digest_size, 0); rb_define_method(cDigest, "block_length", ossl_digest_block_length, 0); diff --git a/test/openssl/test_digest.rb b/test/openssl/test_digest.rb index bc1f680df..b5937786e 100644 --- a/test/openssl/test_digest.rb +++ b/test/openssl/test_digest.rb @@ -139,6 +139,24 @@ def test_fetched_evp_md assert_equal(hex, OpenSSL::Digest.hexdigest("KECCAK-256", "")) end if openssl?(3, 2, 0) + def test_xof_squeeze + if openssl? && !openssl?(3, 3, 0) || libressl? + omit "EVP_DigestSqueeze() is not supported" + end + + # NIST CAVP test vectors, "SHA-3 XOF Test Vectors for Byte-Oriented Output" + # SHAKE128VariableOut.rsp, COUNT = 9 + outputlen = 136 # in bits + msg = ["f167511ec8864979302237abea4cf7ef"].pack("H*") + output = ["20f8938daa54b260860a104f8556278bac"].pack("H*") + + digest = OpenSSL::Digest.new("SHAKE128") + digest.update(msg) + assert_equal(output, digest.dup.squeeze(17)) + assert_equal(outputlen / 8, output.bytesize) + assert_equal(output, digest.squeeze(8) + digest.squeeze(9)) + end + def test_openssl_digest assert_equal OpenSSL::Digest::MD5, OpenSSL::Digest("MD5") From 2a6ed6bb0d89a7e8c716e38787ca9727d953de69 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Mon, 20 Jan 2025 18:48:19 +0900 Subject: [PATCH 2/2] digest: let #finish raise if the output size is unknown In OpenSSL 3.4 or later, SHAKE digests do not have a default output length and EVP_MD_CTX_size() returns -1. Raise OpenSSL::Digest::DigestError if the function returns a negative number. Fortunately, this is currently caught by a sanity check in rb_str_new() and leads to ArgumentError with the message "negative string size (or size too big)". Still, the caller should check it. Also, override #inspect to rescue an exception raised in #hexdigest. --- ext/openssl/ossl_digest.c | 13 ++++++++++++- lib/openssl/digest.rb | 4 ++++ test/openssl/test_digest.rb | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ext/openssl/ossl_digest.c b/ext/openssl/ossl_digest.c index 8223e7d3a..df1a455db 100644 --- a/ext/openssl/ossl_digest.c +++ b/ext/openssl/ossl_digest.c @@ -283,9 +283,20 @@ ossl_digest_finish(VALUE self) { EVP_MD_CTX *ctx; VALUE str; + int size; GetDigest(self, ctx); - str = rb_str_new(NULL, EVP_MD_CTX_size(ctx)); + size = EVP_MD_CTX_size(ctx); + if (size <= 0) { +#ifdef EVP_MD_FLAG_XOF /* Not in LibreSSL 4.3 (latest) */ + if (EVP_MD_flags(EVP_MD_CTX_get0_md(ctx)) & EVP_MD_FLAG_XOF) + ossl_raise(eDigestError, + "output length not set for XOF; " \ + "use OpenSSL::Digest#squeeze instead"); +#endif + ossl_raise(eDigestError, "EVP_MD_CTX_size"); + } + str = rb_str_new(NULL, size); if (!EVP_DigestFinal_ex(ctx, (unsigned char *)RSTRING_PTR(str), NULL)) ossl_raise(eDigestError, "EVP_DigestFinal_ex"); diff --git a/lib/openssl/digest.rb b/lib/openssl/digest.rb index 4e6dea8d0..2cc9d8eee 100644 --- a/lib/openssl/digest.rb +++ b/lib/openssl/digest.rb @@ -52,6 +52,10 @@ def hexdigest(data) class Digest < Digest; end # :nodoc: deprecate_constant :Digest + def inspect + "#<#{self.class}: #{name} #{hexdigest rescue $!.inspect}>" + end + end # Digest # Returns a Digest subclass by _name_ diff --git a/test/openssl/test_digest.rb b/test/openssl/test_digest.rb index b5937786e..443709d8f 100644 --- a/test/openssl/test_digest.rb +++ b/test/openssl/test_digest.rb @@ -157,6 +157,15 @@ def test_xof_squeeze assert_equal(output, digest.squeeze(8) + digest.squeeze(9)) end + def test_xof_digest + # SHAKE had a default output length in OpenSSL < 3.4 + return unless openssl? && openssl?(3, 4, 0) + + digest = OpenSSL::Digest.new("SHAKE128") + assert_raise(OpenSSL::Digest::DigestError) { digest.digest } + assert_match(/#