Bug #120912 REQUIRE SUBJECT/ISSUER authentication fails on OpenSSL 3.x (RHEL 9) due to X509_NAME_oneline() escaping + as \+
Submitted: 13 Jul 15:04
Reporter: Aaditya Dubey Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server: Security: Privileges Severity:S3 (Non-critical)
Version:8.4.10 OS:Any
Assigned to: CPU Architecture:Any

[13 Jul 15:04] Aaditya Dubey
Description:
Users migrating MySQL Server from RHEL 8, which uses OpenSSL 1.1, to RHEL 9, which uses OpenSSL 3.x, may find that existing accounts configured with REQUIRE SUBJECT or REQUIRE ISSUER can no longer authenticate.

The client certificate has not changed, and the value stored in mysql.user.x509_subject remains unchanged. However, OpenSSL 3 formats the same X.509 distinguished name differently by escaping the + character as \+.

MySQL compares the stored value with the runtime certificate subject as raw strings. As a result, authentication fails after the operating-system migration.

Affected Versions
MySQL Community Server 8.4.x on systems using OpenSSL 3.x
MySQL Router 8.4.x
Any MySQL deployment that migrates from OpenSSL 1.1 to OpenSSL 3.x while using REQUIRE SUBJECT or REQUIRE ISSUER

The issue was reproduced with MySQL Community Server 8.4.10.

How to repeat:
1. Create a client certificate with + in the Subject DN

Use a client certificate whose Subject DN contains a + character in one of its field values.

For example:

/DC=Internal CA
/O=Example Corp.
/OU=management:example.group.12345
/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|
/UID=identity:example.group.12345
2. Check the certificate subject on RHEL 8 with OpenSSL 1.1
openssl x509 -noout -subject -nameopt compat -in client-cert.pem

Output:

subject=/DC=Internal CA/O=Example Corp./OU=management:example.group.12345/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|/UID=identity:example.group.12345

The + character is not escaped.

3. Create an account with REQUIRE SUBJECT
CREATE USER 'svcuser'@'%'
REQUIRE SUBJECT
'/DC=Internal CA/O=Example Corp./OU=management:example.group.12345/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|/UID=identity:example.group.12345';

Verify the stored value:

SHOW CREATE USER 'svcuser'@'%';

SELECT user, host, ssl_type, x509_subject
FROM mysql.user
WHERE user = 'svcuser';

MySQL stores the subject with an unescaped +:

...Z5+Ab6...
4. Confirm authentication works on RHEL 8

Connect using the client certificate:

mysql \
  --protocol=TCP \
  --host=127.0.0.1 \
  --port=3306 \
  --user=svcuser \
  --skip-password \
  --ssl-mode=VERIFY_CA \
  --ssl-ca=ca-cert.pem \
  --ssl-cert=client-cert.pem \
  --ssl-key=client-key.pem

The connection succeeds.

5. Migrate the same MySQL data directory and certificates to RHEL 9

Start the same MySQL version on RHEL 9 using:

The same MySQL data directory
The same mysql.user contents
The same CA certificate
The same server certificate
The same client certificate
6. Check the certificate subject on RHEL 9 with OpenSSL 3.x
openssl x509 -noout -subject -nameopt compat -in client-cert.pem

Output:

subject=/DC=Internal CA/O=Example Corp./OU=management:example.group.12345/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5\+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|/UID=identity:example.group.12345

OpenSSL 3 adds a backslash before +.

7. Attempt to connect using the same certificate
mysql \
  --protocol=TCP \
  --host=127.0.0.1 \
  --port=3306 \
  --user=svcuser \
  --skip-password \
  --ssl-mode=VERIFY_CA \
  --ssl-ca=ca-cert.pem \
  --ssl-cert=client-cert.pem \
  --ssl-key=client-key.pem
Actual Result

The connection is rejected:

ERROR 1045 (28000): Access denied for user 'svcuser'@'localhost' (using password: NO)

The MySQL error log reports:

X.509 subject mismatch: should be '/DC=Internal CA/O=Example Corp./OU=management:example.group.12345/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|/UID=identity:example.group.12345' but is '/DC=Internal CA/O=Example Corp./OU=management:example.group.12345/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5\+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|/UID=identity:example.group.12345'

The stored value contains:

...Z5+Ab6...

The runtime certificate subject contains:

...Z5\+Ab6...
Expected Result

Authentication should succeed because:

The client certificate has not changed.
The certificate identity is logically the same.
Only the OpenSSL textual representation of the distinguished name has changed.

The result of X.509 identity verification should not depend on whether the server is linked against OpenSSL 1.1 or OpenSSL 3.x.

Suggested fix:
MySQL uses the legacy OpenSSL function X509_NAME_oneline() to convert X.509 subject and issuer names into strings.

Relevant code paths include:

sql/auth/sql_authentication.cc
  acl_check_ssl()

sql/set_var.cc
  can_persist_non_persistent_var()

router/src/routing/src/router_require.cc
  RouterRequire::enforce()

The stored REQUIRE SUBJECT or REQUIRE ISSUER value is compared directly with the runtime output of X509_NAME_oneline() using raw string comparison.

Conceptually, the comparison becomes:

Stored value:
...Z5+Ab6...

Runtime value on OpenSSL 3:
...Z5\+Ab6...

strcmp("...Z5+Ab6...", "...Z5\+Ab6...") != 0

The certificate is therefore rejected even though both strings represent the same X.509 name.

A related problem occurs when trying to store the OpenSSL 3 representation through a normal SQL string literal:

ALTER USER 'svcuser'@'%'
REQUIRE SUBJECT '...Z5\+Ab6...';

With the default SQL mode, MySQL removes the backslash before the unrecognized \+ escape sequence and stores:

...Z5+Ab6...

Therefore, the OpenSSL 3 representation cannot normally be stored unless NO_BACKSLASH_ESCAPES is enabled.

Impact:

This affects existing X.509-authenticated accounts during operating-system migration from RHEL 8 to RHEL 9 or any migration from OpenSSL 1.1 to OpenSSL 3.x.

Potentially affected features include:

REQUIRE SUBJECT
REQUIRE ISSUER
persist_only_admin_x509_subject
MySQL Router subject and issuer enforcement

In a mixed OpenSSL 1.1 and OpenSSL 3 environment, a single stored string may not work on both systems.

Workaround:

For an environment running only OpenSSL 3.x:

Suggested Fix:

Avoid comparing the textual output of X509_NAME_oneline() directly.

Possible approaches include:

Compare X.509 names structurally using OpenSSL X.509 APIs rather than comparing formatted strings.
Serialize both values into a stable canonical representation before comparison.
Normalize OpenSSL escape differences before comparison.

Any normalization must preserve meaningful literal backslashes and encoded non-printable values while treating equivalent DN representations consistently.

The same correction should be applied to:

Subject comparison
Issuer comparison
persist_only_admin_x509_subject
MySQL Router subject and issuer checks
Verification

On RHEL 8 with OpenSSL 1.1:

openssl x509 -noout -subject -nameopt compat -in client-cert.pem

Output:

subject=.../CN=...Z5+Ab6...

Authentication succeeds.

On RHEL 9 with OpenSSL 3.x:

openssl x509 -noout -subject -nameopt compat -in client-cert.pem

Output:

subject=.../CN=...Z5\+Ab6...

Authentication fails with ER_X509_SUBJECT_MISMATCH.

This confirms that the failure is caused by the version-dependent textual output used for the raw X.509 subject comparison.

SET SESSION sql_mode = 'NO_BACKSLASH_ESCAPES';

ALTER USER 'svcuser'@'%'
REQUIRE SUBJECT
'/DC=Internal CA/O=Example Corp./OU=management:example.group.12345/CN=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5\+Ab6Cd7Ef8Gh9Ij0Kl1Mn2|/UID=identity:example.group.12345';

This stores the literal \+, allowing it to match the OpenSSL 3 runtime output.

However, this value may fail on systems using OpenSSL 1.1, which returns the unescaped + representation.

For mixed OpenSSL 1.1 and OpenSSL 3.x environments, there is no reliable single stored string. A code-level fix is required.