Skip to content

Commit dfbfc02

Browse files
committed
pkey: track whether pkey is private key or not
There are multiple places where it's necessary to know whether a pkey is a private key, a public key, or just key parameters. This is due to two reasons: 1. It's currently a responsibility of the caller to give a properly populated pkey instance to _some_ OpenSSL functions. For example, calling EVP_PKEY_sign() with an RSA pkey instance without the necessary components is known to cause a segfault. 2. OpenSSL::PKey::{RSA,DSA,EC}#to_der behaves differently depending on it: they use the X.509 SubjectPublicKeyInfo structure instead of private key structures if the receiver pkey is a public key. Currently, whenever this is necessary, we check the backing object, such as RSA, and see if the fields are set or not. This approach won't always work on OpenSSL 3.0 because of the architecture change. Unfortunately, OpenSSL doesn't expose an API for this purpose (even though it has one for its internal use). As a workaround, let's manually track this information in an instance variable. This has been partly done for ENGINE-backed pkeys. Now all pkeys get this flag. This is straightforward on OpenSSL 3.0 as pkeys are immutable once instantiated. On OpenSSL 1.1 or before (and current versions of LibreSSL), it must be updated whenever a modification is made to the object. This commit comes with a slight behavior change. Pkeys returned by following method will be explicitly marked as "public", even if it happens to point at an EVP_PKEY struct containing private key components. I expect the effect is minimum since these methods explicitly say "public key". - OpenSSL::X509::Certificate#public_key - OpenSSL::X509::Request#public_key - OpenSSL::Netscape::SPKI#public_key
1 parent 0ab4e67 commit dfbfc02

13 files changed

+288
-241
lines changed

ext/openssl/openssl_missing.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ IMPL_KEY_ACCESSOR3(RSA, crt_params, dmp1, dmq1, iqmp, (dmp1 == obj->dmp1 || dmq1
140140
IMPL_PKEY_GETTER(DSA, dsa)
141141
IMPL_KEY_ACCESSOR2(DSA, key, pub_key, priv_key, (pub_key == obj->pub_key || (obj->priv_key && priv_key == obj->priv_key)))
142142
IMPL_KEY_ACCESSOR3(DSA, pqg, p, q, g, (p == obj->p || q == obj->q || g == obj->g))
143+
static inline ENGINE *DSA_get0_engine(DSA *dsa) { return dsa->engine; }
143144
#endif
144145

145146
#if !defined(OPENSSL_NO_DH)

ext/openssl/ossl_engine.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,7 @@ ossl_engine_load_privkey(int argc, VALUE *argv, VALUE self)
373373
GetEngine(self, e);
374374
pkey = ENGINE_load_private_key(e, sid, NULL, sdata);
375375
if (!pkey) ossl_raise(eEngineError, NULL);
376-
obj = ossl_pkey_new(pkey);
377-
OSSL_PKEY_SET_PRIVATE(obj);
376+
obj = ossl_pkey_new(pkey, OSSL_PKEY_HAS_PRIVATE);
378377

379378
return obj;
380379
}
@@ -403,7 +402,7 @@ ossl_engine_load_pubkey(int argc, VALUE *argv, VALUE self)
403402
pkey = ENGINE_load_public_key(e, sid, NULL, sdata);
404403
if (!pkey) ossl_raise(eEngineError, NULL);
405404

406-
return ossl_pkey_new(pkey);
405+
return ossl_pkey_new(pkey, OSSL_PKEY_HAS_PUBLIC);
407406
}
408407

409408
/*

ext/openssl/ossl_ns_spki.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ ossl_spki_get_public_key(VALUE self)
190190
ossl_raise(eSPKIError, NULL);
191191
}
192192

193-
return ossl_pkey_new(pkey); /* NO DUP - OK */
193+
return ossl_pkey_new(pkey, OSSL_PKEY_HAS_PUBLIC); /* NO DUP - OK */
194194
}
195195

196196
/*

ext/openssl/ossl_pkcs12.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ ossl_pkcs12_s_create(int argc, VALUE *argv, VALUE self)
152152
static VALUE
153153
ossl_pkey_new_i(VALUE arg)
154154
{
155-
return ossl_pkey_new((EVP_PKEY *)arg);
155+
return ossl_pkey_new((EVP_PKEY *)arg, OSSL_PKEY_HAS_PRIVATE);
156156
}
157157

158158
static VALUE

ext/openssl/ossl_pkey.c

Lines changed: 107 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
VALUE mPKey;
2020
VALUE cPKey;
2121
VALUE ePKeyError;
22-
static ID id_private_q;
22+
ID ossl_pkey_feature_id;
2323

2424
static void
2525
ossl_evp_pkey_free(void *ptr)
@@ -65,7 +65,7 @@ pkey_new0(VALUE arg)
6565
}
6666

6767
VALUE
68-
ossl_pkey_new(EVP_PKEY *pkey)
68+
ossl_pkey_new(EVP_PKEY *pkey, enum ossl_pkey_feature ps)
6969
{
7070
VALUE obj;
7171
int status;
@@ -75,6 +75,7 @@ ossl_pkey_new(EVP_PKEY *pkey)
7575
EVP_PKEY_free(pkey);
7676
rb_jump_tag(status);
7777
}
78+
ossl_pkey_set(obj, ps);
7879

7980
return obj;
8081
}
@@ -83,29 +84,28 @@ ossl_pkey_new(EVP_PKEY *pkey)
8384
# include <openssl/decoder.h>
8485

8586
EVP_PKEY *
86-
ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type)
87+
ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type, enum ossl_pkey_feature *ps)
8788
{
8889
void *ppass = (void *)pass;
8990
OSSL_DECODER_CTX *dctx;
9091
EVP_PKEY *pkey = NULL;
9192
int pos = 0, pos2;
93+
size_t i;
9294

9395
dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", NULL, input_type, 0, NULL, NULL);
9496
if (!dctx)
9597
goto out;
9698
if (OSSL_DECODER_CTX_set_pem_password_cb(dctx, ossl_pem_passwd_cb, ppass) != 1)
9799
goto out;
98100

99-
/* First check DER */
100-
if (OSSL_DECODER_from_bio(dctx, bio) == 1)
101-
goto out;
102-
OSSL_BIO_reset(bio);
103-
104-
/* Then check PEM; multiple OSSL_DECODER_from_bio() calls may be needed */
105-
if (OSSL_DECODER_CTX_set_input_type(dctx, "PEM") != 1)
106-
goto out;
107101
/*
108-
* First check for private key formats. This is to keep compatibility with
102+
* This is very inefficient as it will try to decode the same DER/PEM
103+
* encoding repeatedly, but OpenSSL 3.0 doesn't provide an API to return
104+
* what kind of information an EVP_PKEY is holding.
105+
* OpenSSL issue: https://github.com/openssl/openssl/issues/9467
106+
*
107+
* The attempt order of selections is important: private key formats are
108+
* checked first. This is to keep compatibility with
109109
* ruby/openssl < 3.0 which decoded the following as a private key.
110110
*
111111
* $ openssl ecparam -name prime256v1 -genkey -outform PEM
@@ -125,59 +125,94 @@ ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type)
125125
* Note that normally, the input is supposed to contain a single decodable
126126
* PEM block only, so this special handling should not create a new problem.
127127
*/
128-
OSSL_DECODER_CTX_set_selection(dctx, EVP_PKEY_KEYPAIR);
129-
while (1) {
128+
struct { int selection; enum ossl_pkey_feature ps; } selections[] = {
129+
#if OSSL_OPENSSL_PREREQ(3, 0, 0) && !OSSL_OPENSSL_PREREQ(3, 0, 3)
130+
/*
131+
* Public key formats are checked last, with 0 (any) selection to avoid
132+
* segfault in OpenSSL <= 3.0.3.
133+
* Fixed by https://github.com/openssl/openssl/pull/18462
134+
*
135+
* This workaround is mainly for Ubuntu 22.04, which currently has yet
136+
* to backport it (as of 2022-07-26).
137+
*/
138+
{ EVP_PKEY_KEYPAIR, OSSL_PKEY_HAS_PRIVATE },
139+
{ EVP_PKEY_KEY_PARAMETERS, OSSL_PKEY_HAS_NONE },
140+
{ 0, OSSL_PKEY_HAS_PUBLIC },
141+
#else
142+
{ EVP_PKEY_KEYPAIR, OSSL_PKEY_HAS_PRIVATE },
143+
{ EVP_PKEY_PUBLIC_KEY, OSSL_PKEY_HAS_PUBLIC },
144+
{ EVP_PKEY_KEY_PARAMETERS, OSSL_PKEY_HAS_NONE },
145+
#endif
146+
};
147+
148+
/* First check DER */
149+
for (i = 0; i < sizeof(selections)/sizeof(selections[0]); i++) {
150+
OSSL_DECODER_CTX_set_selection(dctx, selections[i].selection);
151+
*ps = selections[i].ps;
130152
if (OSSL_DECODER_from_bio(dctx, bio) == 1)
131153
goto out;
132-
if (BIO_eof(bio))
133-
break;
134-
pos2 = BIO_tell(bio);
135-
if (pos2 < 0 || pos2 <= pos)
136-
break;
137-
ossl_clear_error();
138-
pos = pos2;
154+
OSSL_BIO_reset(bio);
139155
}
140156

141-
OSSL_BIO_reset(bio);
142-
OSSL_DECODER_CTX_set_selection(dctx, 0);
143-
while (1) {
144-
if (OSSL_DECODER_from_bio(dctx, bio) == 1)
145-
goto out;
146-
if (BIO_eof(bio))
147-
break;
148-
pos2 = BIO_tell(bio);
149-
if (pos2 < 0 || pos2 <= pos)
150-
break;
151-
ossl_clear_error();
152-
pos = pos2;
157+
/* Then check PEM; multiple OSSL_DECODER_from_bio() calls may be needed */
158+
if (OSSL_DECODER_CTX_set_input_type(dctx, "PEM") != 1)
159+
goto out;
160+
for (i = 0; i < sizeof(selections)/sizeof(selections[0]); i++) {
161+
OSSL_DECODER_CTX_set_selection(dctx, selections[i].selection);
162+
*ps = selections[i].ps;
163+
while (1) {
164+
if (OSSL_DECODER_from_bio(dctx, bio) == 1)
165+
goto out;
166+
if (BIO_eof(bio))
167+
break;
168+
pos2 = BIO_tell(bio);
169+
if (pos2 < 0 || pos2 <= pos)
170+
break;
171+
ossl_clear_error();
172+
pos = pos2;
173+
}
174+
OSSL_BIO_reset(bio);
153175
}
154176

155177
out:
156178
OSSL_DECODER_CTX_free(dctx);
179+
#if OSSL_OPENSSL_PREREQ(3, 0, 0) && !OSSL_OPENSSL_PREREQ(3, 0, 3)
180+
/* It also leaks an error queue entry in the success path */
181+
if (pkey) ossl_clear_error();
182+
#endif
157183
return pkey;
158184
}
159185
#else
160186
EVP_PKEY *
161-
ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type)
187+
ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type, enum ossl_pkey_feature *ps)
162188
{
163189
void *ppass = (void *)pass;
164190
EVP_PKEY *pkey;
165191

192+
*ps = OSSL_PKEY_HAS_PRIVATE;
166193
if ((pkey = d2i_PrivateKey_bio(bio, NULL)))
167194
goto out;
168195
OSSL_BIO_reset(bio);
169196
if ((pkey = d2i_PKCS8PrivateKey_bio(bio, NULL, ossl_pem_passwd_cb, ppass)))
170197
goto out;
198+
199+
*ps = OSSL_PKEY_HAS_PUBLIC;
171200
OSSL_BIO_reset(bio);
172201
if ((pkey = d2i_PUBKEY_bio(bio, NULL)))
173202
goto out;
174-
OSSL_BIO_reset(bio);
203+
175204
/* PEM_read_bio_PrivateKey() also parses PKCS #8 formats */
205+
*ps = OSSL_PKEY_HAS_PRIVATE;
206+
OSSL_BIO_reset(bio);
176207
if ((pkey = PEM_read_bio_PrivateKey(bio, NULL, ossl_pem_passwd_cb, ppass)))
177208
goto out;
209+
210+
*ps = OSSL_PKEY_HAS_PUBLIC;
178211
OSSL_BIO_reset(bio);
179212
if ((pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL)))
180213
goto out;
214+
215+
*ps = OSSL_PKEY_HAS_NONE;
181216
OSSL_BIO_reset(bio);
182217
if ((pkey = PEM_read_bio_Parameters(bio, NULL)))
183218
goto out;
@@ -234,14 +269,15 @@ ossl_pkey_new_from_data(int argc, VALUE *argv, VALUE self)
234269
EVP_PKEY *pkey;
235270
BIO *bio;
236271
VALUE data, pass;
272+
enum ossl_pkey_feature ps;
237273

238274
rb_scan_args(argc, argv, "11", &data, &pass);
239275
bio = ossl_obj2bio(&data);
240-
pkey = ossl_pkey_read_generic(bio, ossl_pem_passwd_value(pass), NULL);
276+
pkey = ossl_pkey_read_generic(bio, ossl_pem_passwd_value(pass), NULL, &ps);
241277
BIO_free(bio);
242278
if (!pkey)
243279
ossl_raise(ePKeyError, "Could not parse PKey");
244-
return ossl_pkey_new(pkey);
280+
return ossl_pkey_new(pkey, ps);
245281
}
246282

247283
static VALUE
@@ -445,7 +481,7 @@ pkey_generate(int argc, VALUE *argv, VALUE self, int genparam)
445481
}
446482
}
447483

448-
return ossl_pkey_new(gen_arg.pkey);
484+
return ossl_pkey_new(gen_arg.pkey, OSSL_PKEY_HAS_PRIVATE);
449485
}
450486

451487
/*
@@ -567,18 +603,9 @@ GetPrivPKeyPtr(VALUE obj)
567603
EVP_PKEY *pkey;
568604

569605
GetPKey(obj, pkey);
570-
if (OSSL_PKEY_IS_PRIVATE(obj))
571-
return pkey;
572-
/*
573-
* The EVP API does not provide a way to check if the EVP_PKEY has private
574-
* components. Assuming it does...
575-
*/
576-
if (!rb_respond_to(obj, id_private_q))
577-
return pkey;
578-
if (RTEST(rb_funcallv(obj, id_private_q, 0, NULL)))
579-
return pkey;
580-
581-
rb_raise(rb_eArgError, "private key is needed");
606+
if (!ossl_pkey_has(obj, OSSL_PKEY_HAS_PRIVATE))
607+
rb_raise(rb_eArgError, "private key is needed");
608+
return pkey;
582609
}
583610

584611
EVP_PKEY *
@@ -654,6 +681,33 @@ ossl_pkey_oid(VALUE self)
654681
return rb_str_new_cstr(OBJ_nid2sn(nid));
655682
}
656683

684+
/*
685+
* call-seq:
686+
* pkey.public? -> true | false
687+
*
688+
* Indicates whether this PKey instance has a public key associated with it or
689+
* not.
690+
*/
691+
static VALUE
692+
ossl_pkey_is_public(VALUE self)
693+
{
694+
return ossl_pkey_has(self, OSSL_PKEY_HAS_PUBLIC) ? Qtrue : Qfalse;
695+
}
696+
697+
/*
698+
* call-seq:
699+
* pkey.private? -> true | false
700+
*
701+
* Indicates whether this PKey instance has a private key associated with it or
702+
* not.
703+
*/
704+
static VALUE
705+
ossl_pkey_is_private(VALUE self)
706+
{
707+
return ossl_pkey_has(self, OSSL_PKEY_HAS_PRIVATE) ? Qtrue : Qfalse;
708+
}
709+
710+
657711
/*
658712
* call-seq:
659713
* pkey.inspect -> string
@@ -1620,6 +1674,8 @@ Init_ossl_pkey(void)
16201674
rb_undef_method(cPKey, "initialize_copy");
16211675
#endif
16221676
rb_define_method(cPKey, "oid", ossl_pkey_oid, 0);
1677+
rb_define_method(cPKey, "public?", ossl_pkey_is_public, 0);
1678+
rb_define_method(cPKey, "private?", ossl_pkey_is_private, 0);
16231679
rb_define_method(cPKey, "inspect", ossl_pkey_inspect, 0);
16241680
rb_define_method(cPKey, "to_text", ossl_pkey_to_text, 0);
16251681
rb_define_method(cPKey, "private_to_der", ossl_pkey_private_to_der, -1);
@@ -1637,7 +1693,7 @@ Init_ossl_pkey(void)
16371693
rb_define_method(cPKey, "encrypt", ossl_pkey_encrypt, -1);
16381694
rb_define_method(cPKey, "decrypt", ossl_pkey_decrypt, -1);
16391695

1640-
id_private_q = rb_intern("private?");
1696+
ossl_pkey_feature_id = rb_intern_const("state");
16411697

16421698
/*
16431699
* INIT rsa, dsa, dh, ec

ext/openssl/ossl_pkey.h

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ extern VALUE mPKey;
1414
extern VALUE cPKey;
1515
extern VALUE ePKeyError;
1616
extern const rb_data_type_t ossl_evp_pkey_type;
17-
18-
/* For ENGINE */
19-
#define OSSL_PKEY_SET_PRIVATE(obj) rb_ivar_set((obj), rb_intern("private"), Qtrue)
20-
#define OSSL_PKEY_IS_PRIVATE(obj) (rb_attr_get((obj), rb_intern("private")) == Qtrue)
17+
extern ID ossl_pkey_feature_id;
2118

2219
#define GetPKey(obj, pkey) do {\
2320
TypedData_Get_Struct((obj), EVP_PKEY, &ossl_evp_pkey_type, (pkey)); \
@@ -26,10 +23,34 @@ extern const rb_data_type_t ossl_evp_pkey_type;
2623
} \
2724
} while (0)
2825

26+
/*
27+
* Store whether pkey contains public key, private key, or neither of them.
28+
* This is ugly, but OpenSSL currently (3.0) doesn't provide a public API for
29+
* this purpose.
30+
*/
31+
enum ossl_pkey_feature {
32+
OSSL_PKEY_HAS_NONE, /* or parameters only */
33+
OSSL_PKEY_HAS_PUBLIC,
34+
OSSL_PKEY_HAS_PRIVATE,
35+
};
36+
37+
static inline void
38+
ossl_pkey_set(VALUE self, enum ossl_pkey_feature feature)
39+
{
40+
rb_ivar_set(self, ossl_pkey_feature_id, INT2FIX(feature));
41+
}
42+
43+
static inline int
44+
ossl_pkey_has(VALUE self, enum ossl_pkey_feature feature)
45+
{
46+
return FIX2INT(rb_attr_get(self, ossl_pkey_feature_id)) >= (int)feature;
47+
}
48+
2949
/* Takes ownership of the EVP_PKEY */
30-
VALUE ossl_pkey_new(EVP_PKEY *);
50+
VALUE ossl_pkey_new(EVP_PKEY *pkey, enum ossl_pkey_feature ps);
3151
void ossl_pkey_check_public_key(const EVP_PKEY *);
32-
EVP_PKEY *ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type);
52+
EVP_PKEY *ossl_pkey_read_generic(BIO *bio, VALUE pass, const char *input_type,
53+
enum ossl_pkey_feature *ps);
3354
EVP_PKEY *GetPKeyPtr(VALUE);
3455
EVP_PKEY *DupPKeyPtr(VALUE);
3556
EVP_PKEY *GetPrivPKeyPtr(VALUE);
@@ -145,6 +166,7 @@ static VALUE ossl_##_keytype##_set_##_group(VALUE self, VALUE v1, VALUE v2, VALU
145166
BN_clear_free(bn3); \
146167
ossl_raise(ePKeyError, #_type"_set0_"#_group); \
147168
} \
169+
_keytype##_fix_selection(self, obj); \
148170
return self; \
149171
}
150172

@@ -172,6 +194,7 @@ static VALUE ossl_##_keytype##_set_##_group(VALUE self, VALUE v1, VALUE v2) \
172194
BN_clear_free(bn2); \
173195
ossl_raise(ePKeyError, #_type"_set0_"#_group); \
174196
} \
197+
_keytype##_fix_selection(self, obj); \
175198
return self; \
176199
}
177200
#else /* OSSL_HAVE_PROVIDER */

0 commit comments

Comments
 (0)