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

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

Closed
wants to merge 9 commits into from

Conversation

pkarman
Copy link

@pkarman pkarman commented Oct 11, 2016

Why: Supports Probabilistic Signature Scheme for RSA key signing.

See #75

Copy link
Member

@rhenium rhenium left a comment

Choose a reason for hiding this comment

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

Thank you for the PR!

I think we should also provide a way to specify the PSS parameters, the salt length and the MGF. Since MGF1 is the only supported by OpenSSL (and the only used in wild) as the MGF, actually the two parameters would be the salt length and the hash algorithm which will be used in MGF1.


pkey_ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!pkey_ctx)
ossl_raise(ePKeyError, "EVP_PKEY_CTX_new");
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

This leaks. EVP_Digest{Sign,Verify}Init() will allocate a new EVP_PKEY_CTX.

Copy link
Author

Choose a reason for hiding this comment

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

you're saying it leaks because there is no corresponding EVP_PKEY_CTX_free call? I thought the same thing but got a segfault when I included it.

Copy link
Member

Choose a reason for hiding this comment

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

No, EVP_DigestSignInit() will overwrite the pkey_ctx variable and the EVP_PKEY_CTX allocated here is simply lost. The new EVP_PKEY_CTX allocated in EVP_DigestSignInit() will be free'd by EVP_MD_CTX_destroy().

ossl_raise(ePKeyError, "EVP_PKEY_CTX_new");

if (EVP_DigestSignInit(md_ctx, &pkey_ctx, md, NULL, pkey) != 1)
ossl_raise(ePKeyError, "EVP_DigestSignInit");
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

md_ctx is not free'd.


md_ctx = EVP_MD_CTX_new();
if (!md_ctx)
ossl_raise(ePKeyError, "EVP_MD_CTX_new");
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

Use eRSAError (OpenSSL::PKey::RSAError) instead of the generic OpenSSL::PKey::PKeyError in RSA-specific methods.

The indent style is incorrect. Ruby (unfortunately) uses an uncommon style. Replace 8 space characters with a \t.

Copy link
Author

Choose a reason for hiding this comment

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

I would prefer to brace all my if statements instead of relying on whitespace at all but was trying to follow convention I saw in original sign() method.

Is it ok to do:

if (!md_ctx) {
    ossl_raise(ePKeyError, "EVP_MD_CTX_new");
}

Copy link
Member

Choose a reason for hiding this comment

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

I personally prefer to omit brace for one-line if statements but acceptable.

StringValue(signature);
StringValue(data);

if (EVP_PKEY_missing_parameters(pkey))
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

This is unnecessary for RSA keys.

pkey = GetPrivPKeyPtr(self);
md = GetDigestPtr(digest);
StringValue(data);
signature = rb_str_new(0, EVP_PKEY_size(pkey)+16);
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

What is +16? EVP_PKEY_size(pkey) should be enough.

Copy link
Author

Choose a reason for hiding this comment

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

hmm. I lifted that directly from the original sign() method in ext/openssl/ossl_pkey.c

Are you saying that because this is a RSA-specific pkey its size is sufficient for the RSA signature? That would make sense to me.

Copy link
Member

Choose a reason for hiding this comment

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

EVP_PKEY_size() should return the maximum signature size regardless of the key type. The resulting signature may be smaller but not larger. The current code in PKey#sign and also RSA#{public,private}_{encrypt,decrypt}, DSA#syssign and EC#dsa_sign_asn1 must be incorrect.

It seems it was already in the very first commit b24b432#diff-b11f730c7415dc47e71a5ea29bc843b1R361, I'm not sure why... But eventually they will be fixed as well.

Copy link
Member

Choose a reason for hiding this comment

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

Fixed by 0be7f41.

*
* Probabilistic Signature Scheme for RSA sign().
*
* To sign the +String+ +data+, +digest+, an instance of OpenSSL::Digest, must
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

It's not your fault, but this is not really accurate. digest actually can be a String like "SHA1" since Ruby 1.9.0.

Copy link
Author

Choose a reason for hiding this comment

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

good to know. Should I open a separate PR to fix the doc for sign() ?

Copy link
Member

Choose a reason for hiding this comment

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

That's really nice, thanks!

#if (OPENSSL_VERSION_NUMBER >= 0x10000000)
/*
* call-seq:
* rsa.sign_pss -> String
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

(digest, data) is dropped.

void *pkey_ptr;
const BIGNUM *n, *e;

GetPKey(self, pkey);
Copy link
Member

@rhenium rhenium Oct 12, 2016

Choose a reason for hiding this comment

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

Use GetPKeyRSA() macro.


pkey_ptr = EVP_PKEY_get0(pkey);
if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA)
ossl_raise(ePKeyError, "key must be RSA");
Copy link
Member

Choose a reason for hiding this comment

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

This is done in GetPKeyRSA().

if (!result)
ossl_raise(ePKeyError, NULL);

assert((long)buf_len <= RSTRING_LEN(signature));
Copy link
Member

Choose a reason for hiding this comment

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

rb_str_set_len() will do this.

Copy link
Author

Choose a reason for hiding this comment

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

oh good. should I open another PR to change that in the original sign() method as well?

Copy link
Member

Choose a reason for hiding this comment

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

I fixed by 0c187b3.

@pkarman
Copy link
Author

pkarman commented Oct 13, 2016

Thanks for the great review @rhenium. I obviously copy/pasted a bunch from the existing sign|verify methods and then updated (some but not enough) to use the newer openssl API. I will get a follow-up commit posted soon for your review.


result = EVP_DigestSignFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), &buf_len);

EVP_MD_CTX_destroy(md_ctx);
Copy link
Member

@rhenium rhenium Oct 14, 2016

Choose a reason for hiding this comment

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

I forgot to add: use EVP_MD_CTX_free() instead. EVP_MD_CTX_destroy() was renamed in OpenSSL 1.1.0 (we have a compat macro in ext/openssl/openssl_missing.h for old versions of OpenSSL).

@pkarman
Copy link
Author

pkarman commented Oct 14, 2016

@rhenium I pushed 9133300 with refactors according to your feedback. I'm happy to squash before merging, once this PR reaches an approved place.

@pkarman
Copy link
Author

pkarman commented Oct 14, 2016

I realized I missed this comment:

I think we should also provide a way to specify the PSS parameters, the salt length and the MGF. Since MGF1 is the only supported by OpenSSL (and the only used in wild) as the MGF, actually the two parameters would be the salt length and the hash algorithm which will be used in MGF1.

I'll submit another pass with that feature.

@pkarman
Copy link
Author

pkarman commented Oct 14, 2016

I might need some help with the MGF piece. Reading https://www.openssl.org/docs/manmaster/crypto/EVP_PKEY_CTX_ctrl.html I see EVP_PKEY_CTX_set_rsa_pss_saltlen but am puzzled by MGF. Any ideas?

@rhenium
Copy link
Member

rhenium commented Oct 15, 2016

There is an undocumented macro EVP_PKEY_CTX_set_rsa_mgf1_md(). It seems it was added in OpenSSL 1.0.1, so #if needs to be updated.

if (!pkey_ctx) {
error = 1;
goto err;
}
Copy link
Member

Choose a reason for hiding this comment

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

I might not be clear enough. Just remove this. (also from #verify)

}

if (EVP_DigestSignInit(md_ctx, &pkey_ctx, md, NULL, pkey) != 1) {
error = 2;
Copy link
Member

Choose a reason for hiding this comment

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

The value of error is only used to determine if an error happens or not. I think this can be made more simple.

    if (EVP_abc() != 1) {
    goto err;
    }

    if (EVP_def() != 1) {
    goto err;
    }

    EVP_MD_CTX_free(...);
    return signature;

err:
    EVP_MD_CTX_free(...);
    ossl_raise(...)

goto err;
}

EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING);
Copy link
Member

Choose a reason for hiding this comment

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

Check the return value. (also in #verify)


if (EVP_DigestSignFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), &buf_len) != 1) {
error = 4;
goto err;
Copy link
Member

Choose a reason for hiding this comment

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

The indentation should be like:

<four spaces>if (...) {
<one tab character>error = 0;
<four spaces>}

Indentation width is 4, but every 8 consecutive spaces are replaced with one tab character.

rb_str_set_len(signature, buf_len);

err:
EVP_MD_CTX_destroy(md_ctx);
Copy link
Member

Choose a reason for hiding this comment

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

Use EVP_MD_CTX_free().

err:
EVP_MD_CTX_destroy(md_ctx);
if (error) {
ossl_raise(ePKeyError, NULL);
Copy link
Member

Choose a reason for hiding this comment

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

Use eRSAError.

StringValue(data);

pkey_ptr = EVP_PKEY_get0(pkey);
RSA_get0_key(pkey_ptr, &n, &e, NULL);
Copy link
Member

Choose a reason for hiding this comment

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

In this case, EVP_PKEY_get0_RSA() that returns an RSA * should be used to avoid implicit type conversion between void *.

goto err;
}

result = EVP_DigestVerifyFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), RSTRING_LENINT(signature));
Copy link
Member

Choose a reason for hiding this comment

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

RSTRING_LENINT() raises a RangeError if a too long String is passed as the signature, and md_ctx will leak. It is a fault in the original code... I'll fix soon.

Copy link
Member

Choose a reason for hiding this comment

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

Fixed by e9deede.

if (!result)
ossl_raise(ePKeyError, NULL);

assert((long)buf_len <= RSTRING_LEN(signature));
Copy link
Member

Choose a reason for hiding this comment

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

I fixed by 0c187b3.

@pkarman
Copy link
Author

pkarman commented Oct 19, 2016

@rhenium ok I think I've all your comments and added salt + mgf to the pss params. The method signatures are quite long now, and I just don't know if, idiomatically, it makes sense to make salt+mgf optional with sane defaults. I didn't immediately seen other examples of that idiom in this project.

Thoughts?

@tarcieri
Copy link
Collaborator

Since this gem targets 2.3+, why not use keyword arguments?

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.

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, NUM2INT(saltlen)) != 1)
Copy link
Member

Choose a reason for hiding this comment

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

NUM2INT() raises RangeError if saltlen is too big for int, so it must be called before allocating the EVP_MD_CTX. Note that NUM2INT() also performs the type check. (also applies to #verify_pss.)

signature = key.sign_pss(digest, data, salt_len, hash_alg)
assert_equal(true, key.verify_pss(digest, signature, data, salt_len, hash_alg))
assert_equal(false, key.verify_pss(digest, signature, invalid_data, salt_len, hash_alg))
end
Copy link
Member

Choose a reason for hiding this comment

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

This test needs to be skipped if the OpenSSL version does not support PSS.

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

result = EVP_DigestVerifyFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), RSTRING_LENINT(signature));
Copy link
Member

Choose a reason for hiding this comment

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

Calls for RSTRING_LENINT() must be made before allocating md_ctx because it may raise RangeError.

*
* The +Integer+ +salt_len+ should be the salt length to use.
*
* The +String+ +hash_alg+ should be the hash algorithm used in MGF.
Copy link
Member

Choose a reason for hiding this comment

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

Please make it clear that this is a parameter of MGF1, which is just one implementation of MGF (mask generation function).


md_ctx = EVP_MD_CTX_new();
if (!md_ctx)
ossl_raise(eRSAError, "EVP_MD_CTX_new");
Copy link
Member

Choose a reason for hiding this comment

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

Why not use goto err here? EVP_MD_CTX_free() can be called against NULL.

@pkarman
Copy link
Author

pkarman commented Oct 20, 2016

@tarcieri I agree. Are there existing examples of named arguments in this gem?

@rhenium
Copy link
Member

rhenium commented Oct 20, 2016

@pkarman Currently, no. Ruby provides rb_get_kwargs() for this purpose. (see https://github.com/ruby/ruby/blob/6f22fc6b70b989245d6ee4d825ea1d9677779b11/doc/extension.rdoc#method-definition, and there are some usage examples in Ruby core code.)

@pkarman
Copy link
Author

pkarman commented Oct 20, 2016

@rhenium thanks, I have made updates based on your review in fd37542

@pkarman
Copy link
Author

pkarman commented Oct 20, 2016

figuring out named arguments is going to take bit more time than I have this week. I can return to that next week.

@rhenium
Copy link
Member

rhenium commented Nov 7, 2016

@pkarman status? Note that the deadline for Ruby 2.4 inclusion is about the end of this month (see https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/ReleaseEngineering24).

@pkarman
Copy link
Author

pkarman commented Nov 7, 2016

I will get to this this week, thanks for the nudge.

Thoughts on what the preferred Ruby method signature should look like? Ideas:

rsa_key.sign_pss(digest, data, saltlen, hash_alg)  # current, no named args
rsa_key.sign_pss(digest, data, saltlen: saltlen, hash_alg: hash_alg)  # named args for optional values
rsa_key.sign_pss(digest: digest, data: data, saltlen: saltlen, hash_alg: hash_alg)  # named args for everything

I kind of like the 2nd one because it has parity with the existing sign and emphasizes that the optional bits are "extra". OTOH, named args for everything would be most consistent.

Same thing for verify_pss.

@pkarman
Copy link
Author

pkarman commented Nov 14, 2016

@rhenium thoughts on syntax?

@rhenium
Copy link
Member

rhenium commented Nov 15, 2016

I like the second one, too, because the two parameters are distinct.

The keyword names can be better. I don't think we need to inherit the abbreviation 'saltlen' here. 'hash_alg' is not clear because PSS can technically use other MGFs than MGF1, while the hash algorithm is specific to MGF1. I suggest 'mgf1_hash'.

As you might have noticed, the manpage of EVP_PKEY_CTX_set_rsa_pss_saltlen() says it accepts two special values, -1 and -2. I think we should provide a better way rather than forcing users to write the magic number in their code. I imagine something like this:

sig = rsa_key.sign_pss(digest, data, salt_length: OpenSSL::PKey::RSA::PSS_MAX_SALT_LENGTH) # too long?
sig = rsa_key.sign_pss(digest, data, salt_length: :max_length)

@pkarman
Copy link
Author

pkarman commented Nov 21, 2016

I've got some of the named args work started but I can tell I'm not going to get it done before end of the month. I'm happy to wait and do it in Dec. Up to you.

@rhenium
Copy link
Member

rhenium commented Nov 30, 2016

Sorry for the delay, I missed the notification...

That's OK, let's integrate in the next release. In that case, the #ifs can be removed, as we won't support OpenSSL < 1.0.1.

@KevinLapagna
Copy link

Any news on when this will be integrated?

And should it not even be the default (.sign) instead of an alternative function (.sign_pss)? According to the information in this article, the use of these signatures without PSS are considered to be insecure:
Why PKCS#1v1.5 Encryption Should Be Put Out of Our Misery

@rhenium
Copy link
Member

rhenium commented Nov 3, 2017

Ping. I'd be happy to take over this patch and finish the kwargs work so we can make this into our v2.1.0 release which will happen this month.

The behavior of OpenSSL::PKey::RSA#sign is not changing, though, because of compatibility. A PR that improves the documentation would be welcome.

@karpet
Copy link

karpet commented Nov 3, 2017

Sorry to delay so long on this @rhenium - I moved on from the project I was working on that needed it.
IIRC I got hung up on figuring out how to do the named args.
Happy to have you pick it up and finish if you have time.

rhenium pushed a commit to rhenium/ruby-openssl that referenced this pull request Nov 13, 2017
Support Probabilistic Signature Scheme for RSA key signing.

[ky: the patch was originally submitted as GitHub Pull Request ruby#76.
 finish keyword arguments handling, update docs, and fix tests.]
@rhenium
Copy link
Member

rhenium commented Nov 13, 2017

See #169.

@rhenium rhenium closed this Nov 13, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants