-
Notifications
You must be signed in to change notification settings - Fork 171
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
Conversation
There was a problem hiding this 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.
ext/openssl/ossl_pkey_rsa.c
Outdated
|
||
pkey_ctx = EVP_PKEY_CTX_new(pkey, NULL); | ||
if (!pkey_ctx) | ||
ossl_raise(ePKeyError, "EVP_PKEY_CTX_new"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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().
ext/openssl/ossl_pkey_rsa.c
Outdated
ossl_raise(ePKeyError, "EVP_PKEY_CTX_new"); | ||
|
||
if (EVP_DigestSignInit(md_ctx, &pkey_ctx, md, NULL, pkey) != 1) | ||
ossl_raise(ePKeyError, "EVP_DigestSignInit"); |
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
|
||
md_ctx = EVP_MD_CTX_new(); | ||
if (!md_ctx) | ||
ossl_raise(ePKeyError, "EVP_MD_CTX_new"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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");
}
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
StringValue(signature); | ||
StringValue(data); | ||
|
||
if (EVP_PKEY_missing_parameters(pkey)) |
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
pkey = GetPrivPKeyPtr(self); | ||
md = GetDigestPtr(digest); | ||
StringValue(data); | ||
signature = rb_str_new(0, EVP_PKEY_size(pkey)+16); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by 0be7f41.
ext/openssl/ossl_pkey_rsa.c
Outdated
* | ||
* Probabilistic Signature Scheme for RSA sign(). | ||
* | ||
* To sign the +String+ +data+, +digest+, an instance of OpenSSL::Digest, must |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
?
There was a problem hiding this comment.
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!
ext/openssl/ossl_pkey_rsa.c
Outdated
#if (OPENSSL_VERSION_NUMBER >= 0x10000000) | ||
/* | ||
* call-seq: | ||
* rsa.sign_pss -> String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(digest, data)
is dropped.
ext/openssl/ossl_pkey_rsa.c
Outdated
void *pkey_ptr; | ||
const BIGNUM *n, *e; | ||
|
||
GetPKey(self, pkey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use GetPKeyRSA() macro.
ext/openssl/ossl_pkey_rsa.c
Outdated
|
||
pkey_ptr = EVP_PKEY_get0(pkey); | ||
if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA) | ||
ossl_raise(ePKeyError, "key must be RSA"); |
There was a problem hiding this comment.
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().
ext/openssl/ossl_pkey_rsa.c
Outdated
if (!result) | ||
ossl_raise(ePKeyError, NULL); | ||
|
||
assert((long)buf_len <= RSTRING_LEN(signature)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed by 0c187b3.
Thanks for the great review @rhenium. I obviously copy/pasted a bunch from the existing |
ext/openssl/ossl_pkey_rsa.c
Outdated
|
||
result = EVP_DigestSignFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), &buf_len); | ||
|
||
EVP_MD_CTX_destroy(md_ctx); |
There was a problem hiding this comment.
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).
I realized I missed this comment:
I'll submit another pass with that feature. |
I might need some help with the MGF piece. Reading https://www.openssl.org/docs/manmaster/crypto/EVP_PKEY_CTX_ctrl.html I see |
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. |
ext/openssl/ossl_pkey_rsa.c
Outdated
if (!pkey_ctx) { | ||
error = 1; | ||
goto err; | ||
} |
There was a problem hiding this comment.
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)
ext/openssl/ossl_pkey_rsa.c
Outdated
} | ||
|
||
if (EVP_DigestSignInit(md_ctx, &pkey_ctx, md, NULL, pkey) != 1) { | ||
error = 2; |
There was a problem hiding this comment.
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(...)
ext/openssl/ossl_pkey_rsa.c
Outdated
goto err; | ||
} | ||
|
||
EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING); |
There was a problem hiding this comment.
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)
ext/openssl/ossl_pkey_rsa.c
Outdated
|
||
if (EVP_DigestSignFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), &buf_len) != 1) { | ||
error = 4; | ||
goto err; |
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
rb_str_set_len(signature, buf_len); | ||
|
||
err: | ||
EVP_MD_CTX_destroy(md_ctx); |
There was a problem hiding this comment.
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().
ext/openssl/ossl_pkey_rsa.c
Outdated
err: | ||
EVP_MD_CTX_destroy(md_ctx); | ||
if (error) { | ||
ossl_raise(ePKeyError, NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use eRSAError.
ext/openssl/ossl_pkey_rsa.c
Outdated
StringValue(data); | ||
|
||
pkey_ptr = EVP_PKEY_get0(pkey); | ||
RSA_get0_key(pkey_ptr, &n, &e, NULL); |
There was a problem hiding this comment.
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 *
.
ext/openssl/ossl_pkey_rsa.c
Outdated
goto err; | ||
} | ||
|
||
result = EVP_DigestVerifyFinal(md_ctx, (unsigned char *)RSTRING_PTR(signature), RSTRING_LENINT(signature)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by e9deede.
ext/openssl/ossl_pkey_rsa.c
Outdated
if (!result) | ||
ossl_raise(ePKeyError, NULL); | ||
|
||
assert((long)buf_len <= RSTRING_LEN(signature)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed by 0c187b3.
**Why**: Supports Probabilistic Signature Scheme for RSA key signing.
@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? |
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) |
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
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) |
There was a problem hiding this comment.
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.)
test/test_pkey_rsa.rb
Outdated
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 |
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
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)); |
There was a problem hiding this comment.
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.
ext/openssl/ossl_pkey_rsa.c
Outdated
* | ||
* The +Integer+ +salt_len+ should be the salt length to use. | ||
* | ||
* The +String+ +hash_alg+ should be the hash algorithm used in MGF. |
There was a problem hiding this comment.
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).
ext/openssl/ossl_pkey_rsa.c
Outdated
|
||
md_ctx = EVP_MD_CTX_new(); | ||
if (!md_ctx) | ||
ossl_raise(eRSAError, "EVP_MD_CTX_new"); |
There was a problem hiding this comment.
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.
@tarcieri I agree. Are there existing examples of named arguments in this gem? |
@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.) |
figuring out named arguments is going to take bit more time than I have this week. I can return to that next week. |
@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). |
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 Same thing for |
@rhenium thoughts on syntax? |
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) |
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. |
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. |
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: |
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. |
Sorry to delay so long on this @rhenium - I moved on from the project I was working on that needed it. |
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.]
See #169. |
Why: Supports Probabilistic Signature Scheme for RSA key signing.
See #75