Skip to content

Add RSA sign_pss() and verify_pss() methods #76

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

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions ext/openssl/ossl_pkey_rsa.c
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,201 @@ ossl_rsa_to_public_key(VALUE self)
return obj;
}

#if (OPENSSL_VERSION_NUMBER >= 0x1000100f)
/*
* call-seq:
* rsa.sign_pss(digest, data, options) -> String
*
* Probabilistic Signature Scheme for RSA sign().
*
* To sign the +String+ +data+, +digest+ must be provided.
* +digest+ must be a OpenSSL::Digest instance or +String+ like "SHA1".
*
* The +Hash+ +options+ supports two key/value pairs:
*
* The +Integer+ +salt_length+ should be the salt length to use.
* Two special values are reserved: -1 means use the digest length, and
* -2 means use the maximum possible length. If not specified, a default
* value of -2 is assumed. The symbols +:digest_length+ and +:max_length+
* may be used instead.
*
* The +String+ +mgf1_hash+ should be the hash algorithm used in MGF1
* (the currently supported mask generation function (MGF)).
*
* The return value is again a +String+ containing the signature.
* A RSAError is raised should errors occur.
* Any previous state of the +Digest+ instance is irrelevant to the signature
* outcome, the digest instance is reset to its initial state during the
* operation.
*
* == Example
* data = 'Sign me!'
* digest = OpenSSL::Digest::SHA256.new
* pkey = OpenSSL::PKey::RSA.new(2048)
* signature = pkey.sign_pss(digest, data, salt_length: 20, mgf1_hash: 'SHA256')
*/
static VALUE
ossl_rsa_sign_pss(int argc, VALUE *argv, VALUE self)
{
VALUE digest, data, options;
const ID options_table[] = {saltlen, mgf1_hash};
VALUE kwvals[sizeof(options_table) / sizeof(*options_table)];
EVP_PKEY *pkey;
EVP_PKEY_CTX *pkey_ctx;
const EVP_MD *md, *mgf1md;
EVP_MD_CTX *md_ctx;
size_t buf_len;
int salt_len, n_args;
VALUE signature;

n_args = rb_scan_args(argc, argv, "21:", &digest, &data, &options);
if (n_args == 3)
rb_get_kwargs(options, options_table, 0, 2, kwvals);

if (kwvals[0] == Qundef) {
salt_len = -2; // default
} else {
salt_len = NUM2INT(kwvals[0]);
}

salt_len = NUM2INT(saltlen);
pkey = GetPrivPKeyPtr(self);
md = GetDigestPtr(digest);
mgf1md = GetDigestPtr(mgf1_hash);
StringValue(data);
signature = rb_str_new(0, EVP_PKEY_size(pkey));
buf_len = EVP_PKEY_size(pkey);

md_ctx = EVP_MD_CTX_new();
if (!md_ctx)
goto err;

if (EVP_DigestSignInit(md_ctx, &pkey_ctx, md, NULL, pkey) != 1)
goto err;

if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING) != 1)
goto err;

if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_len) != 1)
goto err;

if (EVP_PKEY_CTX_set_rsa_mgf1_md(pkey_ctx, mgf1md) != 1)
goto err;

if (EVP_DigestSignUpdate(md_ctx, RSTRING_PTR(data), RSTRING_LEN(data)) != 1)
goto err;

if (EVP_DigestSignFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), &buf_len) != 1)
Copy link
Member

Choose a reason for hiding this comment

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

I overlooked in previous reviews, but buf_len needs to be set to EVP_PKEY_size(pkey) before calling EVP_DigestSignFinal(). Travis CI seems to be broken because of this.

https://www.openssl.org/docs/manmaster/crypto/EVP_DigestSignInit.html:

EVP_DigestSignFinal() signs the data in ctx places the signature in sig. If sig is NULL then the maximum size of the output buffer is written to the siglen parameter. If sig is not NULL then before the call the siglen parameter should contain the length of the sig buffer, if the call is successful the signature is written to sig and the amount of data written to siglen.

goto err;

rb_str_set_len(signature, buf_len);

EVP_MD_CTX_free(md_ctx);
return signature;

err:
EVP_MD_CTX_free(md_ctx);
ossl_raise(eRSAError, NULL);
}

/*
* call-seq:
* pkey.verify_pss(digest, signature, data, options) -> String
*
* Probabilistic Signature Scheme for RSA verify().
*
* To verify the +String+ +signature+, +digest+ and +data+ must be provided
* to re-compute the message digest. See +sign_pss+ for details about
* +digest+ and +data+.
*
* The +Hash+ +options+ supports two key/value pairs:
*
* The +Integer+ +salt_length+ should be the salt length to use. Must be the same value
* used in sign_pss().
*
* The +String+ +mgf1_hash+ should be the hash algorithm used in MGF1
* (the currently supported mask generation function (MGF)). Must be the same value
* used in sign_pss().
*
* The return value is +true+ if the
* signature is valid, +false+ otherwise. A PKeyError is raised should errors
* occur.
* Any previous state of the +Digest+ instance is irrelevant to the validation
* outcome, the digest instance is reset to its initial state during the
* operation.
*
* == Example
* data = 'Sign me!'
* digest = OpenSSL::Digest::SHA256.new
* pkey = OpenSSL::PKey::RSA.new(2048)
* signature = pkey.sign_pss(digest, data, salt_length: 20, mgf1_hash: 'SHA256')
* pub_key = pkey.public_key
* puts pub_key.verify_pss(digest, signature, data, salt_length: 20, mgf1_hash: 'SHA256') # => true
*/
static VALUE
ossl_rsa_verify_pss(VALUE self, VALUE digest, VALUE signature, VALUE data, VALUE saltlen, VALUE mgf1_hash)
{
EVP_PKEY *pkey;
EVP_PKEY_CTX *pkey_ctx;
const EVP_MD *md, *mgf1md;
EVP_MD_CTX *md_ctx;
int result, salt_len, sig_len;
RSA *rsa;
const BIGNUM *rsa_n;

salt_len = NUM2INT(saltlen);
sig_len = RSTRING_LENINT(signature);
GetPKeyRSA(self, pkey);
md = GetDigestPtr(digest);
mgf1md = GetDigestPtr(mgf1_hash);
StringValue(signature);
StringValue(data);

GetRSA(self, rsa);
RSA_get0_key(rsa, &rsa_n, NULL, NULL);
if (!rsa_n)
ossl_raise(eRSAError, "incomplete RSA");

md_ctx = EVP_MD_CTX_new();
if (!md_ctx)
goto err;

if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, md, NULL, pkey) != 1)
goto err;

if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING) != 1)
goto err;

if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_len) != 1)
goto err;

if (EVP_PKEY_CTX_set_rsa_mgf1_md(pkey_ctx, mgf1md) != 1)
goto err;

if (EVP_DigestVerifyUpdate(md_ctx, RSTRING_PTR(data), RSTRING_LEN(data)) != 1)
goto err;

result = EVP_DigestVerifyFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), sig_len);

switch (result) {
case 0:
ossl_clear_error();
EVP_MD_CTX_free(md_ctx);
return Qfalse;
case 1:
EVP_MD_CTX_free(md_ctx);
return Qtrue;
default:
goto err;
}

err:
EVP_MD_CTX_free(md_ctx);
ossl_raise(eRSAError, NULL);

}
#endif

/*
* TODO: Test me

Expand Down Expand Up @@ -731,6 +926,10 @@ Init_ossl_rsa(void)
rb_define_method(cRSA, "public_decrypt", ossl_rsa_public_decrypt, -1);
rb_define_method(cRSA, "private_encrypt", ossl_rsa_private_encrypt, -1);
rb_define_method(cRSA, "private_decrypt", ossl_rsa_private_decrypt, -1);
#if (OPENSSL_VERSION_NUMBER >= 0x1000100f)
rb_define_method(cRSA, "sign_pss", ossl_rsa_sign_pss, -1);
rb_define_method(cRSA, "verify_pss", ossl_rsa_verify_pss, -1);
#endif

DEF_OSSL_PKEY_BN(cRSA, rsa, n);
DEF_OSSL_PKEY_BN(cRSA, rsa, e);
Expand Down
40 changes: 40 additions & 0 deletions test/test_pkey_rsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@
class OpenSSL::TestPKeyRSA < OpenSSL::PKeyTestCase
RSA1024 = OpenSSL::TestUtils::TEST_KEY_RSA1024

def test_sign_verify_pss
if OpenSSL::OPENSSL_VERSION_NUMBER > 0x1000100f
key = OpenSSL::PKey::RSA.new(512, 3)
digest = OpenSSL::Digest::SHA1.new
salt_len = 20
hash_alg = 'SHA1'
options = { salt_length: salt_len, mgf1_hash: hash_alg }
data = "Sign me!"
invalid_data = "Sign me?"

signature = key.sign_pss(digest, data, salt_length: salt_len, mgf1_hash: hash_alg)
assert_equal(true, key.verify_pss(digest, signature, data, salt_length: salt_len, mgf1_hash: hash_alg))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, salt_length: salt_len, mgf1_hash: hash_alg))

signature = key.sign_ss(digest, data, salt_length: salt_len)
assert_equal(true, key.verify_pss(digest, signature, data, salt_length: salt_len))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, salt_length: salt_len))

signature = key.sign_ss(digest, data, mgf1_hash: hash_alg)
assert_equal(true, key.verify_pss(digest, signature, data, mgf1_hash: hash_alg))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, mgf1_hash: hash_alg))

signature = key.sign_ss(digest, data)
assert_equal(true, key.verify_pss(digest, signature, data))
assert_equal(false, key.verify_pss(digest, signature, invalid_data))

signature = key.sign_ss(digest, data, options)
assert_equal(true, key.verify_pss(digest, signature, data, options))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, options))

signature = key.sign_ss(digest, data, salt_length: :digest_length)
assert_equal(true, key.verify_pss(digest, signature, data, salt_length: :digest_length))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, salt_length: :digest_length))

signature = key.sign_ss(digest, data, salt_length: :max_length)
assert_equal(true, key.verify_pss(digest, signature, data, salt_length: :max_length))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, salt_length: :max_length))
end
end

def test_padding
key = OpenSSL::PKey::RSA.new(512, 3)

Expand Down