From 4ebff35971d499f4ddd13f48bff0444f77d63421 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 22 May 2020 16:10:35 +0900 Subject: [ruby/openssl] pkey: implement PKey#sign_raw, #verify_raw, and #verify_recover Add a variant of PKey#sign and #verify that do not hash the data automatically. Sometimes the caller has the hashed data only, but not the plaintext to be signed. In that case, users would have to use the low-level API such as RSA#private_encrypt or #public_decrypt directly. OpenSSL 1.0.0 and later supports EVP_PKEY_sign() and EVP_PKEY_verify() which provide the same functionality as part of the EVP API. This patch adds wrappers for them. https://github.com/ruby/openssl/commit/16cca4e0c4 --- ext/openssl/ossl_pkey.c | 232 ++++++++++++++++++++++++++++++++++++++++++ test/openssl/test_pkey_dsa.rb | 25 ++++- test/openssl/test_pkey_ec.rb | 21 +++- test/openssl/test_pkey_rsa.rb | 78 +++++++++----- 4 files changed, 325 insertions(+), 31 deletions(-) diff --git a/ext/openssl/ossl_pkey.c b/ext/openssl/ossl_pkey.c index 6416c4b105..203ab789ca 100644 --- a/ext/openssl/ossl_pkey.c +++ b/ext/openssl/ossl_pkey.c @@ -973,6 +973,235 @@ ossl_pkey_verify(int argc, VALUE *argv, VALUE self) } } +/* + * call-seq: + * pkey.sign_raw(digest, data [, options]) -> string + * + * Signs +data+ using a private key +pkey+. Unlike #sign, +data+ will not be + * hashed by +digest+ automatically. + * + * See #verify_raw for the verification operation. + * + * Added in version 3.0. See also the man page EVP_PKEY_sign(3). + * + * +digest+:: + * A String that represents the message digest algorithm name, or +nil+ + * if the PKey type requires no digest algorithm. + * Although this method will not hash +data+ with it, this parameter may still + * be required depending on the signature algorithm. + * +data+:: + * A String. The data to be signed. + * +options+:: + * A Hash that contains algorithm specific control operations to \OpenSSL. + * See OpenSSL's man page EVP_PKEY_CTX_ctrl_str(3) for details. + * + * Example: + * data = "Sign me!" + * hash = OpenSSL::Digest.digest("SHA256", data) + * pkey = OpenSSL::PKey.generate_key("RSA", rsa_keygen_bits: 2048) + * signopts = { rsa_padding_mode: "pss" } + * signature = pkey.sign_raw("SHA256", hash, signopts) + * + * # Creates a copy of the RSA key pkey, but without the private components + * pub_key = pkey.public_key + * puts pub_key.verify_raw("SHA256", signature, hash, signopts) # => true + */ +static VALUE +ossl_pkey_sign_raw(int argc, VALUE *argv, VALUE self) +{ + EVP_PKEY *pkey; + VALUE digest, data, options, sig; + const EVP_MD *md = NULL; + EVP_PKEY_CTX *ctx; + size_t outlen; + int state; + + GetPKey(self, pkey); + rb_scan_args(argc, argv, "21", &digest, &data, &options); + if (!NIL_P(digest)) + md = ossl_evp_get_digestbyname(digest); + StringValue(data); + + ctx = EVP_PKEY_CTX_new(pkey, /* engine */NULL); + if (!ctx) + ossl_raise(ePKeyError, "EVP_PKEY_CTX_new"); + if (EVP_PKEY_sign_init(ctx) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_sign_init"); + } + if (md && EVP_PKEY_CTX_set_signature_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_CTX_set_signature_md"); + } + if (!NIL_P(options)) { + pkey_ctx_apply_options(ctx, options, &state); + if (state) { + EVP_PKEY_CTX_free(ctx); + rb_jump_tag(state); + } + } + if (EVP_PKEY_sign(ctx, NULL, &outlen, (unsigned char *)RSTRING_PTR(data), + RSTRING_LEN(data)) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_sign"); + } + if (outlen > LONG_MAX) { + EVP_PKEY_CTX_free(ctx); + rb_raise(ePKeyError, "signature would be too large"); + } + sig = ossl_str_new(NULL, (long)outlen, &state); + if (state) { + EVP_PKEY_CTX_free(ctx); + rb_jump_tag(state); + } + if (EVP_PKEY_sign(ctx, (unsigned char *)RSTRING_PTR(sig), &outlen, + (unsigned char *)RSTRING_PTR(data), + RSTRING_LEN(data)) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_sign"); + } + EVP_PKEY_CTX_free(ctx); + rb_str_set_len(sig, outlen); + return sig; +} + +/* + * call-seq: + * pkey.verify_raw(digest, signature, data [, options]) -> true or false + * + * Verifies the +signature+ for the +data+ using a public key +pkey+. Unlike + * #verify, this method will not hash +data+ with +digest+ automatically. + * + * Returns +true+ if the signature is successfully verified, +false+ otherwise. + * The caller must check the return value. + * + * See #sign_raw for the signing operation and an example code. + * + * Added in version 3.0. See also the man page EVP_PKEY_verify(3). + * + * +signature+:: + * A String containing the signature to be verified. + */ +static VALUE +ossl_pkey_verify_raw(int argc, VALUE *argv, VALUE self) +{ + EVP_PKEY *pkey; + VALUE digest, sig, data, options; + const EVP_MD *md = NULL; + EVP_PKEY_CTX *ctx; + int state, ret; + + GetPKey(self, pkey); + rb_scan_args(argc, argv, "31", &digest, &sig, &data, &options); + ossl_pkey_check_public_key(pkey); + if (!NIL_P(digest)) + md = ossl_evp_get_digestbyname(digest); + StringValue(sig); + StringValue(data); + + ctx = EVP_PKEY_CTX_new(pkey, /* engine */NULL); + if (!ctx) + ossl_raise(ePKeyError, "EVP_PKEY_CTX_new"); + if (EVP_PKEY_verify_init(ctx) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_verify_init"); + } + if (md && EVP_PKEY_CTX_set_signature_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_CTX_set_signature_md"); + } + if (!NIL_P(options)) { + pkey_ctx_apply_options(ctx, options, &state); + if (state) { + EVP_PKEY_CTX_free(ctx); + rb_jump_tag(state); + } + } + ret = EVP_PKEY_verify(ctx, (unsigned char *)RSTRING_PTR(sig), + RSTRING_LEN(sig), + (unsigned char *)RSTRING_PTR(data), + RSTRING_LEN(data)); + EVP_PKEY_CTX_free(ctx); + if (ret < 0) + ossl_raise(ePKeyError, "EVP_PKEY_verify"); + + if (ret) + return Qtrue; + else { + ossl_clear_error(); + return Qfalse; + } +} + +/* + * call-seq: + * pkey.verify_recover(digest, signature [, options]) -> string + * + * Recovers the signed data from +signature+ using a public key +pkey+. Not all + * signature algorithms support this operation. + * + * Added in version 3.0. See also the man page EVP_PKEY_verify_recover(3). + * + * +signature+:: + * A String containing the signature to be verified. + */ +static VALUE +ossl_pkey_verify_recover(int argc, VALUE *argv, VALUE self) +{ + EVP_PKEY *pkey; + VALUE digest, sig, options, out; + const EVP_MD *md = NULL; + EVP_PKEY_CTX *ctx; + int state; + size_t outlen; + + GetPKey(self, pkey); + rb_scan_args(argc, argv, "21", &digest, &sig, &options); + ossl_pkey_check_public_key(pkey); + if (!NIL_P(digest)) + md = ossl_evp_get_digestbyname(digest); + StringValue(sig); + + ctx = EVP_PKEY_CTX_new(pkey, /* engine */NULL); + if (!ctx) + ossl_raise(ePKeyError, "EVP_PKEY_CTX_new"); + if (EVP_PKEY_verify_recover_init(ctx) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_verify_recover_init"); + } + if (md && EVP_PKEY_CTX_set_signature_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_CTX_set_signature_md"); + } + if (!NIL_P(options)) { + pkey_ctx_apply_options(ctx, options, &state); + if (state) { + EVP_PKEY_CTX_free(ctx); + rb_jump_tag(state); + } + } + if (EVP_PKEY_verify_recover(ctx, NULL, &outlen, + (unsigned char *)RSTRING_PTR(sig), + RSTRING_LEN(sig)) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_verify_recover"); + } + out = ossl_str_new(NULL, (long)outlen, &state); + if (state) { + EVP_PKEY_CTX_free(ctx); + rb_jump_tag(state); + } + if (EVP_PKEY_verify_recover(ctx, (unsigned char *)RSTRING_PTR(out), &outlen, + (unsigned char *)RSTRING_PTR(sig), + RSTRING_LEN(sig)) <= 0) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_verify_recover"); + } + EVP_PKEY_CTX_free(ctx); + rb_str_set_len(out, outlen); + return out; +} + /* * call-seq: * pkey.derive(peer_pkey) -> string @@ -1262,6 +1491,9 @@ Init_ossl_pkey(void) rb_define_method(cPKey, "sign", ossl_pkey_sign, -1); rb_define_method(cPKey, "verify", ossl_pkey_verify, -1); + rb_define_method(cPKey, "sign_raw", ossl_pkey_sign_raw, -1); + rb_define_method(cPKey, "verify_raw", ossl_pkey_verify_raw, -1); + rb_define_method(cPKey, "verify_recover", ossl_pkey_verify_recover, -1); rb_define_method(cPKey, "derive", ossl_pkey_derive, -1); rb_define_method(cPKey, "encrypt", ossl_pkey_encrypt, -1); rb_define_method(cPKey, "decrypt", ossl_pkey_decrypt, -1); diff --git a/test/openssl/test_pkey_dsa.rb b/test/openssl/test_pkey_dsa.rb index 85bb6ec0ae..147e50176b 100644 --- a/test/openssl/test_pkey_dsa.rb +++ b/test/openssl/test_pkey_dsa.rb @@ -48,12 +48,31 @@ class OpenSSL::TestPKeyDSA < OpenSSL::PKeyTestCase assert_equal false, dsa512.verify("SHA256", signature1, data) end - def test_sys_sign_verify - key = Fixtures.pkey("dsa256") + def test_sign_verify_raw + key = Fixtures.pkey("dsa512") data = 'Sign me!' digest = OpenSSL::Digest.digest('SHA1', data) + + invalid_sig = key.sign_raw(nil, digest.succ) + malformed_sig = "*" * invalid_sig.bytesize + + # Sign by #syssign sig = key.syssign(digest) - assert(key.sysverify(digest, sig)) + assert_equal true, key.sysverify(digest, sig) + assert_equal false, key.sysverify(digest, invalid_sig) + assert_raise(OpenSSL::PKey::DSAError) { key.sysverify(digest, malformed_sig) } + assert_equal true, key.verify_raw(nil, sig, digest) + assert_equal false, key.verify_raw(nil, invalid_sig, digest) + assert_raise(OpenSSL::PKey::PKeyError) { key.verify_raw(nil, malformed_sig, digest) } + + # Sign by #sign_raw + sig = key.sign_raw(nil, digest) + assert_equal true, key.sysverify(digest, sig) + assert_equal false, key.sysverify(digest, invalid_sig) + assert_raise(OpenSSL::PKey::DSAError) { key.sysverify(digest, malformed_sig) } + assert_equal true, key.verify_raw(nil, sig, digest) + assert_equal false, key.verify_raw(nil, invalid_sig, digest) + assert_raise(OpenSSL::PKey::PKeyError) { key.verify_raw(nil, malformed_sig, digest) } end def test_DSAPrivateKey diff --git a/test/openssl/test_pkey_ec.rb b/test/openssl/test_pkey_ec.rb index 80ae9ffdf1..0a460bd536 100644 --- a/test/openssl/test_pkey_ec.rb +++ b/test/openssl/test_pkey_ec.rb @@ -109,13 +109,30 @@ class OpenSSL::TestEC < OpenSSL::PKeyTestCase assert_equal a.derive(b), a.dh_compute_key(b.public_key) end - def test_dsa_sign_verify + def test_sign_verify_raw + key = Fixtures.pkey("p256") data1 = "foo" data2 = "bar" - key = OpenSSL::PKey::EC.new("prime256v1").generate_key! + + malformed_sig = "*" * 30 + + # Sign by #dsa_sign_asn1 sig = key.dsa_sign_asn1(data1) assert_equal true, key.dsa_verify_asn1(data1, sig) assert_equal false, key.dsa_verify_asn1(data2, sig) + assert_raise(OpenSSL::PKey::ECError) { key.dsa_verify_asn1(data1, malformed_sig) } + assert_equal true, key.verify_raw(nil, sig, data1) + assert_equal false, key.verify_raw(nil, sig, data2) + assert_raise(OpenSSL::PKey::PKeyError) { key.verify_raw(nil, malformed_sig, data1) } + + # Sign by #sign_raw + sig = key.sign_raw(nil, data1) + assert_equal true, key.dsa_verify_asn1(data1, sig) + assert_equal false, key.dsa_verify_asn1(data2, sig) + assert_raise(OpenSSL::PKey::ECError) { key.dsa_verify_asn1(data1, malformed_sig) } + assert_equal true, key.verify_raw(nil, sig, data1) + assert_equal false, key.verify_raw(nil, sig, data2) + assert_raise(OpenSSL::PKey::PKeyError) { key.verify_raw(nil, malformed_sig, data1) } end def test_dsa_sign_asn1_FIPS186_3 diff --git a/test/openssl/test_pkey_rsa.rb b/test/openssl/test_pkey_rsa.rb index d6bfca3ac5..5e127f5407 100644 --- a/test/openssl/test_pkey_rsa.rb +++ b/test/openssl/test_pkey_rsa.rb @@ -13,32 +13,6 @@ class OpenSSL::TestPKeyRSA < OpenSSL::PKeyTestCase assert_raise(OpenSSL::PKey::RSAError){ key.private_decrypt("foo") } end - def test_padding - key = OpenSSL::PKey::RSA.new(512, 3) - - # Need right size for raw mode - plain0 = "x" * (512/8) - cipher = key.private_encrypt(plain0, OpenSSL::PKey::RSA::NO_PADDING) - plain1 = key.public_decrypt(cipher, OpenSSL::PKey::RSA::NO_PADDING) - assert_equal(plain0, plain1) - - # Need smaller size for pkcs1 mode - plain0 = "x" * (512/8 - 11) - cipher1 = key.private_encrypt(plain0, OpenSSL::PKey::RSA::PKCS1_PADDING) - plain1 = key.public_decrypt(cipher1, OpenSSL::PKey::RSA::PKCS1_PADDING) - assert_equal(plain0, plain1) - - cipherdef = key.private_encrypt(plain0) # PKCS1_PADDING is default - plain1 = key.public_decrypt(cipherdef) - assert_equal(plain0, plain1) - assert_equal(cipher1, cipherdef) - - # Failure cases - assert_raise(ArgumentError){ key.private_encrypt() } - assert_raise(ArgumentError){ key.private_encrypt("hi", 1, nil) } - assert_raise(OpenSSL::PKey::RSAError){ key.private_encrypt(plain0, 666) } - end - def test_private # Generated by key size and public exponent key = OpenSSL::PKey::RSA.new(512, 3) @@ -133,6 +107,58 @@ class OpenSSL::TestPKeyRSA < OpenSSL::PKeyTestCase assert_equal false, key.verify("SHA256", sig_pss, data) end + def test_sign_verify_raw + key = Fixtures.pkey("rsa-1") + data = "Sign me!" + hash = OpenSSL::Digest.digest("SHA1", data) + signature = key.sign_raw("SHA1", hash) + assert_equal true, key.verify_raw("SHA1", signature, hash) + assert_equal true, key.verify("SHA1", signature, data) + + # Too long data + assert_raise(OpenSSL::PKey::PKeyError) { + key.sign_raw("SHA1", "x" * (key.n.num_bytes + 1)) + } + + # With options + pssopts = { + "rsa_padding_mode" => "pss", + "rsa_pss_saltlen" => 20, + "rsa_mgf1_md" => "SHA256" + } + sig_pss = key.sign_raw("SHA1", hash, pssopts) + assert_equal true, key.verify("SHA1", sig_pss, data, pssopts) + assert_equal true, key.verify_raw("SHA1", sig_pss, hash, pssopts) + end + + def test_sign_verify_raw_legacy + key = Fixtures.pkey("rsa-1") + bits = key.n.num_bits + + # Need right size for raw mode + plain0 = "x" * (bits/8) + cipher = key.private_encrypt(plain0, OpenSSL::PKey::RSA::NO_PADDING) + plain1 = key.public_decrypt(cipher, OpenSSL::PKey::RSA::NO_PADDING) + assert_equal(plain0, plain1) + + # Need smaller size for pkcs1 mode + plain0 = "x" * (bits/8 - 11) + cipher1 = key.private_encrypt(plain0, OpenSSL::PKey::RSA::PKCS1_PADDING) + plain1 = key.public_decrypt(cipher1, OpenSSL::PKey::RSA::PKCS1_PADDING) + assert_equal(plain0, plain1) + + cipherdef = key.private_encrypt(plain0) # PKCS1_PADDING is default + plain1 = key.public_decrypt(cipherdef) + assert_equal(plain0, plain1) + assert_equal(cipher1, cipherdef) + + # Failure cases + assert_raise(ArgumentError){ key.private_encrypt() } + assert_raise(ArgumentError){ key.private_encrypt("hi", 1, nil) } + assert_raise(OpenSSL::PKey::RSAError){ key.private_encrypt(plain0, 666) } + end + + def test_verify_empty_rsa rsa = OpenSSL::PKey::RSA.new assert_raise(OpenSSL::PKey::PKeyError, "[Bug #12783]") { -- cgit v1.2.3