Blob Blame History Raw
From 40451afa279c52ce7a508f8a9ec553cfe7a76a10 Mon Sep 17 00:00:00 2001
From: Jun Aruga <jaruga@redhat.com>
Date: Wed, 12 Apr 2023 17:15:21 +0200
Subject: [PATCH] Fix OpenSSL::PKey.read in OpenSSL 3 FIPS module.

This is a combination of the following 2 commits. Because the combined patch is
easy to merge.

This is the 1st commit message:

[ruby/openssl] Workaround: Fix OpenSSL::PKey.read that cannot parse PKey in the FIPS mode.

This commit is a workaround to avoid the error below that the
`OpenSSL::PKey.read` fails with the OpenSSL 3.0 FIPS mode.

```
$ openssl genrsa -out key.pem 4096

$ ruby -e "require 'openssl'; OpenSSL::PKey.read(File.read('key.pem'))"
-e:1:in `read': Could not parse PKey (OpenSSL::PKey::PKeyError)
  from -e:1:in `<main>'
```

The root cause is on the OpenSSL side. The `OSSL_DECODER_CTX_set_selection`
doesn't apply the selection value properly if there are multiple providers, and
a provider (e.g.  "base" provider) handles the decoder implementation, and
another provider (e.g. "fips" provider) handles the keys.

The workaround is to create `OSSL_DECODER_CTX` variable each time without using
the `OSSL_DECODER_CTX_set_selection`.

https://github.com/ruby/openssl/commit/5ff4a31621

This is the commit message #2:

[ruby/openssl] ossl_pkey.c: Workaround: Decode with non-zero selections.

This is a workaround for the decoding issue in ossl_pkey_read_generic().
The issue happens in the case that a key management provider is different from
a decoding provider.

Try all the non-zero selections in order, instead of selection 0 for OpenSSL 3
to avoid the issue.

https://github.com/ruby/openssl/commit/db688fa739
---
 ext/openssl/ossl_pkey.c | 78 ++++++++++++++++++++++++++++++++++++++---
 1 file changed, 73 insertions(+), 5 deletions(-)

diff --git a/ext/openssl/ossl_pkey.c b/ext/openssl/ossl_pkey.c
index 476256679b..419422958e 100644
--- a/ext/openssl/ossl_pkey.c
+++ b/ext/openssl/ossl_pkey.c
@@ -82,18 +82,20 @@ ossl_pkey_new(EVP_PKEY *pkey)
 #if OSSL_OPENSSL_PREREQ(3, 0, 0)
 # include <openssl/decoder.h>
 
-EVP_PKEY *
-ossl_pkey_read_generic(BIO *bio, VALUE pass)
+static EVP_PKEY *
+ossl_pkey_read(BIO *bio, const char *input_type, int selection, VALUE pass)
 {
     void *ppass = (void *)pass;
     OSSL_DECODER_CTX *dctx;
     EVP_PKEY *pkey = NULL;
     int pos = 0, pos2;
 
-    dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", NULL, NULL, 0, NULL, NULL);
+    dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, input_type, NULL, NULL,
+                                         selection, NULL, NULL);
     if (!dctx)
         goto out;
-    if (OSSL_DECODER_CTX_set_pem_password_cb(dctx, ossl_pem_passwd_cb, ppass) != 1)
+    if (OSSL_DECODER_CTX_set_pem_password_cb(dctx, ossl_pem_passwd_cb,
+                                             ppass) != 1)
         goto out;
 
     /* First check DER */
@@ -151,11 +153,77 @@ ossl_pkey_read_generic(BIO *bio, VALUE pass)
         ossl_clear_error();
         pos = pos2;
     }
-
   out:
+    OSSL_BIO_reset(bio);
     OSSL_DECODER_CTX_free(dctx);
     return pkey;
 }
+
+EVP_PKEY *
+ossl_pkey_read_generic(BIO *bio, VALUE pass)
+{
+    EVP_PKEY *pkey = NULL;
+    /* First check DER, then check PEM. */
+    const char *input_types[] = {"DER", "PEM"};
+    int input_type_num = (int)(sizeof(input_types) / sizeof(char *));
+    /*
+     * Non-zero selections to try to decode.
+     *
+     * See EVP_PKEY_fromdata(3) - Selections to see all the selections.
+     *
+     * This is a workaround for the decoder failing to decode or returning
+     * bogus keys with selection 0, if a key management provider is different
+     * from a decoder provider. The workaround is to avoid using selection 0.
+     *
+     * Affected OpenSSL versions: >= 3.1.0, <= 3.1.2, or >= 3.0.0, <= 3.0.10
+     * Fixed OpenSSL versions: 3.2, next release of the 3.1.z and 3.0.z
+     *
+     * See https://github.com/openssl/openssl/pull/21519 for details.
+     *
+     * First check for private key formats (EVP_PKEY_KEYPAIR). This is to keep
+     * compatibility with ruby/openssl < 3.0 which decoded the following as a
+     * private key.
+     *
+     *     $ openssl ecparam -name prime256v1 -genkey -outform PEM
+     *     -----BEGIN EC PARAMETERS-----
+     *     BggqhkjOPQMBBw==
+     *     -----END EC PARAMETERS-----
+     *     -----BEGIN EC PRIVATE KEY-----
+     *     MHcCAQEEIAG8ugBbA5MHkqnZ9ujQF93OyUfL9tk8sxqM5Wv5tKg5oAoGCCqGSM49
+     *     AwEHoUQDQgAEVcjhJfkwqh5C7kGuhAf8XaAjVuG5ADwb5ayg/cJijCgs+GcXeedj
+     *     86avKpGH84DXUlB23C/kPt+6fXYlitUmXQ==
+     *     -----END EC PRIVATE KEY-----
+     *
+     * While the first PEM block is a proper encoding of ECParameters, thus
+     * OSSL_DECODER_from_bio() would pick it up, ruby/openssl used to return
+     * the latter instead. Existing applications expect this behavior.
+     *
+     * Note that normally, the input is supposed to contain a single decodable
+     * PEM block only, so this special handling should not create a new problem.
+     *
+     * Note that we need to create the OSSL_DECODER_CTX variable each time when
+     * we use the different selection as a workaround.
+     * See https://github.com/openssl/openssl/issues/20657 for details.
+     */
+    int selections[] = {
+        EVP_PKEY_KEYPAIR,
+        EVP_PKEY_KEY_PARAMETERS,
+        EVP_PKEY_PUBLIC_KEY
+    };
+    int selection_num = (int)(sizeof(selections) / sizeof(int));
+    int i, j;
+
+    for (i = 0; i < input_type_num; i++) {
+        for (j = 0; j < selection_num; j++) {
+            pkey = ossl_pkey_read(bio, input_types[i], selections[j], pass);
+            if (pkey) {
+                goto out;
+            }
+        }
+    }
+  out:
+    return pkey;
+}
 #else
 EVP_PKEY *
 ossl_pkey_read_generic(BIO *bio, VALUE pass)
-- 
2.41.0