This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new ef3d1a22dba SOLR-18235 Remove support for old PKI auth v1 (#4405)
ef3d1a22dba is described below

commit ef3d1a22dba176b047691f4a76a76dfc1abf6ed9
Author: Jan Høydahl <[email protected]>
AuthorDate: Sun May 10 15:52:31 2026 +0200

    SOLR-18235 Remove support for old PKI auth v1 (#4405)
---
 changelog/unreleased/SOLR-18235-remove-pki-v1.yml  |   7 +
 .../solr/security/PKIAuthenticationPlugin.java     | 163 +++------------------
 .../apache/solr/servlet/AuthenticationFilter.java  |   4 +-
 .../src/java/org/apache/solr/util/CryptoKeys.java  |  50 -------
 .../test/org/apache/solr/cloud/TestRSAKeyPair.java |  66 ---------
 .../solr/security/TestPKIAuthenticationPlugin.java | 122 ++-------------
 .../authentication-and-authorization-plugins.adoc  |  14 +-
 .../pages/major-changes-in-solr-10.adoc            |   8 +
 8 files changed, 53 insertions(+), 381 deletions(-)

diff --git a/changelog/unreleased/SOLR-18235-remove-pki-v1.yml 
b/changelog/unreleased/SOLR-18235-remove-pki-v1.yml
new file mode 100644
index 00000000000..11b3789c928
--- /dev/null
+++ b/changelog/unreleased/SOLR-18235-remove-pki-v1.yml
@@ -0,0 +1,7 @@
+title: Remove support for PKI Authentication v1. The system properties 
solr.pki.sendVersion and solr.pki.acceptVersions are no longer recognized.
+type: removed
+authors:
+  - name: Jan Høydahl
+links:
+  - name: SOLR-18235
+    url: https://issues.apache.org/jira/browse/SOLR-18235
diff --git 
a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java 
b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
index 437a7687548..7c2eb80c28b 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -27,17 +27,14 @@ import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
-import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
 import java.security.Principal;
 import java.security.PublicKey;
 import java.security.SignatureException;
 import java.time.Instant;
 import java.util.Base64;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
@@ -47,7 +44,6 @@ import 
org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.SuppressForbidden;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.request.SolrRequestInfo;
@@ -60,9 +56,6 @@ import org.slf4j.LoggerFactory;
 public class PKIAuthenticationPlugin extends AuthenticationPlugin
     implements HttpClientBuilderPlugin {
 
-  public static final String ACCEPT_VERSIONS = "solr.pki.acceptVersions";
-  public static final String SEND_VERSION = "solr.pki.sendVersion";
-
   /**
    * Mark the current thread as a server thread and set a flag in 
SolrRequestInfo to indicate you
    * want to send a request as the server identity instead of as the 
authenticated user.
@@ -80,24 +73,15 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
 
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  /** If a number has less than this number of digits, it'll not be considered 
a timestamp. */
-  private static final int MIN_TIMESTAMP_DIGITS = 10; // a timestamp of 
9999999999 is year 1970
-
-  /** If a number has more than this number of digits, it'll not be considered 
a timestamp. */
-  private static final int MAX_TIMESTAMP_DIGITS = 13; // a timestamp of 
9999999999999 is year 2286
-
   private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
   private final PublicKeyHandler publicKeyHandler;
   private final CoreContainer cores;
   private final LoadingCache<String, PKIHeaderData> validatedHeaderCache;
-  private final LoadingCache<String, String> generatedV1TokenCache;
   private final LoadingCache<String, String> generatedV2TokenCache;
   private static final int MAX_VALIDITY = Integer.getInteger("pkiauth.ttl", 
10000);
   private final String myNodeName;
   private boolean interceptorRegistered = false;
 
-  private boolean acceptPkiV1 = false;
-
   public boolean isInterceptorRegistered() {
     return interceptorRegistered;
   }
@@ -130,34 +114,12 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
     // runway for requests to come in to trigger an asynchronous-refresh 
before expiry causes a
     // synchronous-refresh.
     long shouldRefreshTime = Math.max(1, expireAfterTime / 2);
-    generatedV1TokenCache =
-        Caffeine.newBuilder()
-            .maximumSize(100)
-            .refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
-            .expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
-            .build(this::generateToken);
     generatedV2TokenCache =
         Caffeine.newBuilder()
             .maximumSize(100)
             .refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
             .expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
             .build(this::generateTokenV2);
-
-    Set<String> knownPkiVersions = Set.of("v1", "v2");
-    // We always accept v2 even if it is not specified
-    String[] versions = System.getProperty(ACCEPT_VERSIONS, "v2").split(",");
-    for (String version : versions) {
-      if (knownPkiVersions.contains(version) == false) {
-        log.warn("Unknown protocol version [{}] specified in {}", version, 
ACCEPT_VERSIONS);
-      }
-      if ("v1".equals(version)) {
-        log.warn(
-            "System setting {} includes the deprecated v1, which should only 
be used for compatibility during rolling upgrades. "
-                + "After all servers have been upgraded, consider disabling 
this compatability layer.",
-            ACCEPT_VERSIONS);
-        acceptPkiV1 = true;
-      }
-    }
   }
 
   @Override
@@ -171,35 +133,25 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
     // Getting the received time must be the first thing we do, processing the 
request can take time
     long receivedTime = System.currentTimeMillis();
 
-    PKIHeaderData headerData = null;
     String headerV2 = request.getHeader(HEADER_V2);
-    String headerV1 = request.getHeader(HEADER);
-    if (headerV1 == null && headerV2 == null) {
-      return sendError(response, true, "No PKI auth header was provided");
-    } else if (headerV2 != null) {
-      // Try V2 first
-      int nodeNameEnd = headerV2.indexOf(' ');
-      if (nodeNameEnd <= 0) {
-        // Do not log the value as it is likely gibberish
-        return sendError(response, true, "Could not parse node name from 
SolrAuthV2 header.");
-      }
+    if (headerV2 == null) {
+      return sendError(response, "No PKI auth header was provided");
+    }
 
-      headerData = validatedHeaderCache.get(headerV2);
-    } else if (headerV1 != null && acceptPkiV1) {
-      List<String> authInfo = StrUtils.splitWS(headerV1, false);
-      if (authInfo.size() != 2) {
-        // We really shouldn't be logging and returning this, but we did it 
before so keep that
-        return sendError(response, false, "Invalid SolrAuth header: " + 
headerV1);
-      }
-      headerData = decipherHeader(authInfo.get(0), authInfo.get(1));
+    int nodeNameEnd = headerV2.indexOf(' ');
+    if (nodeNameEnd <= 0) {
+      // Do not log the value as it is likely gibberish
+      return sendError(response, "Could not parse node name from SolrAuthV2 
header.");
     }
 
+    PKIHeaderData headerData = validatedHeaderCache.get(headerV2);
+
     if (headerData == null) {
-      return sendError(response, true, "Could not validate PKI header.");
+      return sendError(response, "Could not validate PKI header.");
     }
     long elapsed = receivedTime - headerData.timestamp;
     if (elapsed > MAX_VALIDITY) {
-      return sendError(response, true, "Expired key request timestamp, 
elapsed=" + elapsed);
+      return sendError(response, "Expired key request timestamp, elapsed=" + 
elapsed);
     }
 
     final Principal principal =
@@ -217,16 +169,14 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
    * authentication
    *
    * @param response the response to set error status with
-   * @param v2 whether this authentication used the v1 or v2 header (true if 
v2)
-   * @param message the message to log and send back to client. do not include 
anyhting sensitive
+   * @param message the message to log and send back to client. do not include 
anything sensitive
    *     here about server state
    * @return false to chain with calls from authenticate
    */
-  private boolean sendError(HttpServletResponse response, boolean v2, String 
message)
-      throws IOException {
+  private boolean sendError(HttpServletResponse response, String message) 
throws IOException {
     numErrors.inc();
     log.error(message);
-    response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), v2 ? HEADER_V2 
: HEADER);
+    response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), HEADER_V2);
     response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);
     return false;
   }
@@ -304,53 +254,6 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
     }
   }
 
-  private PKIHeaderData decipherHeader(String nodeName, String cipherBase64) {
-    PublicKey key = getOrFetchPublicKey(nodeName);
-
-    PKIHeaderData header = parseCipher(cipherBase64, key, false);
-    if (header == null) {
-      log.warn("Failed to decrypt header, trying after refreshing the key ");
-      key = fetchPublicKeyFromRemote(nodeName);
-      return parseCipher(cipherBase64, key, true);
-    } else {
-      return header;
-    }
-  }
-
-  @VisibleForTesting
-  static PKIHeaderData parseCipher(String cipher, PublicKey key, boolean 
isRetry) {
-    byte[] bytes;
-    try {
-      bytes = CryptoKeys.decryptRSA(Base64.getDecoder().decode(cipher), key);
-    } catch (Exception e) {
-      if (isRetry) {
-        log.error("Decryption failed on retry, key must be wrong", e);
-      } else {
-        log.info("Decryption failed on first attempt, will retry", e);
-      }
-      return null;
-    }
-    String s = new String(bytes, UTF_8).trim();
-    int splitPoint = s.lastIndexOf(' ');
-    int timestampDigits = s.length() - 1 - splitPoint;
-    if (splitPoint == -1
-        || timestampDigits < MIN_TIMESTAMP_DIGITS
-        || timestampDigits > MAX_TIMESTAMP_DIGITS) {
-      log.warn("Invalid cipher {} deciphered data {}", cipher, s);
-      return null;
-    }
-    PKIHeaderData headerData = new PKIHeaderData();
-    try {
-      headerData.timestamp = Long.parseLong(s.substring(splitPoint + 1));
-      headerData.userName = s.substring(0, splitPoint);
-      log.debug("Successfully decrypted header {} {}", headerData.userName, 
headerData.timestamp);
-      return headerData;
-    } catch (NumberFormatException e) {
-      log.warn("Invalid cipher {}", cipher);
-      return null;
-    }
-  }
-
   private boolean isInLiveNodes(String nodeName) {
     return cores
         .getZkController()
@@ -439,16 +342,10 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
             log.trace("onBegin: {}", request);
 
             final Optional<String> preFetchedUser = 
getUserFromJettyRequest(request);
-            if ("v1".equals(System.getProperty(SEND_VERSION))) {
-              preFetchedUser
-                  .map(generatedV1TokenCache::get)
-                  .ifPresent(token -> request.headers(httpFields -> 
httpFields.add(HEADER, token)));
-            } else {
-              preFetchedUser
-                  .map(generatedV2TokenCache::get)
-                  .ifPresent(
-                      token -> request.headers(httpFields -> 
httpFields.add(HEADER_V2, token)));
-            }
+            preFetchedUser
+                .map(generatedV2TokenCache::get)
+                .ifPresent(
+                    token -> request.headers(httpFields -> 
httpFields.add(HEADER_V2, token)));
           }
 
           private void cachePreFetchedUserOnJettyRequest(Request request) {
@@ -492,17 +389,6 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
     }
   }
 
-  @SuppressForbidden(reason = "Needs currentTimeMillis to set current time in 
header")
-  private String generateToken(String usr) {
-    assert usr != null;
-    String s = usr + " " + System.currentTimeMillis();
-    byte[] payload = s.getBytes(UTF_8);
-    byte[] payloadCipher = 
publicKeyHandler.getKeyPair().encrypt(ByteBuffer.wrap(payload));
-    String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
-    log.trace("generateToken: usr={} token={}", usr, base64Cipher);
-    return myNodeName + " " + base64Cipher;
-  }
-
   private String generateTokenV2(String user) {
     assert user != null;
     String s = myNodeName + " " + user + " " + Instant.now().toEpochMilli();
@@ -515,15 +401,9 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
 
   @VisibleForTesting
   void setHeader(BiConsumer<String, String> httpRequest) {
-    if ("v1".equals(System.getProperty(SEND_VERSION))) {
-      getUser()
-          .map(generatedV1TokenCache::get)
-          .ifPresent(token -> httpRequest.accept(HEADER, token));
-    } else {
-      getUser()
-          .map(generatedV2TokenCache::get)
-          .ifPresent(token -> httpRequest.accept(HEADER_V2, token));
-    }
+    getUser()
+        .map(generatedV2TokenCache::get)
+        .ifPresent(token -> httpRequest.accept(HEADER_V2, token));
   }
 
   boolean isSolrThread() {
@@ -545,7 +425,6 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
     return publicKeyHandler.getKeyPair().getPublicKeyStr();
   }
 
-  public static final String HEADER = "SolrAuth";
   public static final String HEADER_V2 = "SolrAuthV2";
   public static final String NODE_IS_USER = "$";
   // special principal to denote the cluster member
diff --git 
a/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java 
b/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
index e842d555d28..abdf8865e3c 100644
--- a/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
@@ -147,10 +147,8 @@ public class AuthenticationFilter extends 
CoreContainerAwareHttpFilter {
   }
 
   private boolean isInternodePKI(HttpServletRequest req, CoreContainer cores) {
-    String header = req.getHeader(PKIAuthenticationPlugin.HEADER);
     String headerV2 = req.getHeader(PKIAuthenticationPlugin.HEADER_V2);
-    return (header != null || headerV2 != null)
-        && cores.getPkiAuthenticationSecurityBuilder() != null;
+    return headerV2 != null && cores.getPkiAuthenticationSecurityBuilder() != 
null;
   }
 
   private void logAuthAttempt(HttpServletRequest req) {
diff --git a/solr/core/src/java/org/apache/solr/util/CryptoKeys.java 
b/solr/core/src/java/org/apache/solr/util/CryptoKeys.java
index 11177b371c0..4150be45753 100644
--- a/solr/core/src/java/org/apache/solr/util/CryptoKeys.java
+++ b/solr/core/src/java/org/apache/solr/util/CryptoKeys.java
@@ -35,7 +35,6 @@ import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
-import java.security.interfaces.RSAPrivateKey;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
@@ -45,9 +44,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
 import org.apache.solr.common.SolrException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,8 +52,6 @@ import org.slf4j.LoggerFactory;
 public final class CryptoKeys {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  private static final String CIPHER_ALGORITHM = "RSA/ECB/nopadding";
-
   private final Map<String, PublicKey> keys;
   private Exception exception;
 
@@ -168,18 +162,6 @@ public final class CryptoKeys {
     }
   }
 
-  public static byte[] decryptRSA(byte[] buffer, PublicKey pubKey)
-      throws InvalidKeyException, BadPaddingException, 
IllegalBlockSizeException {
-    Cipher rsaCipher;
-    try {
-      rsaCipher = Cipher.getInstance(CIPHER_ALGORITHM);
-    } catch (Exception e) {
-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
-    }
-    rsaCipher.init(Cipher.DECRYPT_MODE, pubKey);
-    return rsaCipher.doFinal(buffer);
-  }
-
   public static boolean verifySha256(byte[] data, byte[] sig, PublicKey key)
       throws SignatureException, InvalidKeyException {
     try {
@@ -246,8 +228,6 @@ public final class CryptoKeys {
     // into security.json. Also see SOLR-12103.
     private static final int DEFAULT_KEYPAIR_LENGTH = 2048;
 
-    private final int keySizeInBytes;
-
     /** Create an RSA key pair with newly generated keys. */
     public RSAKeyPair() {
       KeyPairGenerator keyGen;
@@ -259,7 +239,6 @@ public final class CryptoKeys {
       keyGen.initialize(DEFAULT_KEYPAIR_LENGTH);
       KeyPair keyPair = keyGen.genKeyPair();
       privateKey = keyPair.getPrivate();
-      keySizeInBytes = determineKeySizeInBytes(privateKey);
       publicKey = keyPair.getPublic();
       pubKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded());
     }
@@ -284,7 +263,6 @@ public final class CryptoKeys {
             new 
PKCS8EncodedKeySpec(Base64.getMimeDecoder().decode(privateString));
         KeyFactory rsaFactory = KeyFactory.getInstance("RSA");
         privateKey = rsaFactory.generatePrivate(privateSpec);
-        keySizeInBytes = determineKeySizeInBytes(privateKey);
       } catch (NoSuchAlgorithmException e) {
         throw new AssertionError("JVM spec is required to support RSA", e);
       }
@@ -303,34 +281,6 @@ public final class CryptoKeys {
       return publicKey;
     }
 
-    private int determineKeySizeInBytes(PrivateKey privateKey) {
-      return ((RSAPrivateKey) privateKey).getModulus().bitLength() / Byte.SIZE;
-    }
-
-    // Used for testing
-    public int getKeySizeInBytes() {
-      return keySizeInBytes;
-    }
-
-    public byte[] encrypt(ByteBuffer buffer) {
-      // This is necessary to pad the plaintext to match the exact size of the 
keysize in openj9.
-      // OpenJDK seems to do this padding internally, but OpenJ9 does not pad 
the byte input to
-      // the key size in bytes without padding. This only works with 
"RSA/ECB/nopadding".
-      byte[] paddedPlaintext = new byte[getKeySizeInBytes()];
-      buffer.get(paddedPlaintext, buffer.arrayOffset() + buffer.position(), 
buffer.limit());
-
-      try {
-        // This is better than nothing, but still not very secure
-        // See:
-        // 
https://crypto.stackexchange.com/questions/20085/which-attacks-are-possible-against-raw-textbook-rsa
-        Cipher rsaCipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        rsaCipher.init(Cipher.ENCRYPT_MODE, privateKey);
-        return rsaCipher.doFinal(paddedPlaintext);
-      } catch (Exception e) {
-        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
-      }
-    }
-
     public byte[] signSha256(byte[] bytes) {
       Signature dsa = null;
       try {
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestRSAKeyPair.java 
b/solr/core/src/test/org/apache/solr/cloud/TestRSAKeyPair.java
deleted file mode 100644
index e24beda4d2c..00000000000
--- a/solr/core/src/test/org/apache/solr/cloud/TestRSAKeyPair.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.solr.cloud;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.not;
-
-import java.net.URL;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import org.apache.lucene.tests.util.TestUtil;
-import org.apache.solr.SolrTestCase;
-import org.apache.solr.util.CryptoKeys;
-import org.junit.Test;
-
-public class TestRSAKeyPair extends SolrTestCase {
-  @Test
-  public void testGenKeyPair() throws Exception {
-    testRoundTrip(new CryptoKeys.RSAKeyPair());
-  }
-
-  @Test
-  public void testReadKeysFromDisk() throws Exception {
-    URL privateKey = 
getClass().getClassLoader().getResource("cryptokeys/priv_key512_pkcs8.pem");
-    URL publicKey = 
getClass().getClassLoader().getResource("cryptokeys/pub_key512.der");
-    assertNotNull(privateKey);
-    assertNotNull(publicKey);
-    testRoundTrip(new CryptoKeys.RSAKeyPair(privateKey, publicKey));
-  }
-
-  private void testRoundTrip(CryptoKeys.RSAKeyPair kp) throws Exception {
-    int keySizeInBytes = kp.getKeySizeInBytes();
-    // Max size of the plaintext can only be as big as the key in bytes with 
no padding
-    String plaintextString = TestUtil.randomSimpleString(random(), 
keySizeInBytes);
-    final byte[] plaintext = plaintextString.getBytes(StandardCharsets.UTF_8);
-
-    byte[] encrypted = kp.encrypt(ByteBuffer.wrap(plaintext));
-    assertThat(plaintext, not(equalTo(encrypted)));
-
-    byte[] decrypted = CryptoKeys.decryptRSA(encrypted, kp.getPublicKey());
-
-    assertTrue(
-        "Decrypted text is shorter than original text.", decrypted.length >= 
plaintext.length);
-
-    // Strip off any null bytes RSAKeyPair uses RSA/ECB/NoPadding and during 
decryption null bytes
-    // can be left.
-    // Under "Known Limitations"
-    // 
https://www.ibm.com/docs/en/sdk-java-technology/8?topic=guide-ibmjceplus-ibmjceplusfips-providers
-    assertArrayEquals(plaintext, Arrays.copyOf(decrypted, plaintext.length));
-  }
-}
diff --git 
a/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java 
b/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java
index f5d79ffddea..cff82ba6e7a 100644
--- 
a/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java
+++ 
b/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java
@@ -16,10 +16,10 @@
  */
 package org.apache.solr.security;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -30,11 +30,8 @@ import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletRequest;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import java.nio.ByteBuffer;
 import java.security.Principal;
 import java.security.PublicKey;
-import java.time.Instant;
-import java.util.Base64;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
@@ -85,8 +82,6 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
       (servletRequest, servletResponse) -> 
wrappedRequestByFilter.set(servletRequest);
   final String nodeName = "node_x_233";
 
-  final CryptoKeys.RSAKeyPair aKeyPair = new CryptoKeys.RSAKeyPair();
-
   final SolrQueryRequestBase solrQueryRequestBase =
       new SolrQueryRequestBase(null, new ModifiableSolrParams());
 
@@ -104,15 +99,7 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
     headerValue.set(null);
     wrappedRequestByFilter.set(null);
 
-    if (random().nextBoolean()) {
-      headerKey = PKIAuthenticationPlugin.HEADER_V2;
-      System.setProperty(PKIAuthenticationPlugin.SEND_VERSION, "v2");
-      System.setProperty(PKIAuthenticationPlugin.ACCEPT_VERSIONS, "v2");
-    } else {
-      headerKey = PKIAuthenticationPlugin.HEADER;
-      System.setProperty(PKIAuthenticationPlugin.SEND_VERSION, "v1");
-      System.setProperty(PKIAuthenticationPlugin.ACCEPT_VERSIONS, "v1,v2");
-    }
+    headerKey = PKIAuthenticationPlugin.HEADER_V2;
 
     mockReq = createMockRequest(headerValue);
     mock = new MockPKIAuthenticationPlugin(nodeName);
@@ -197,21 +184,21 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
   }
 
   @Test
-  public void testProtocolMismatch() throws Exception {
-    System.setProperty(PKIAuthenticationPlugin.SEND_VERSION, "v1");
-    System.setProperty(PKIAuthenticationPlugin.ACCEPT_VERSIONS, "v2");
-    mock = new MockPKIAuthenticationPlugin(nodeName);
-    mockMetrics(mock);
-
-    principal.set(new SimplePrincipal("solr"));
-    mock.solrRequestInfo = new SolrRequestInfo(solrQueryRequestBase, new 
SolrQueryResponse());
-    mockSetHeaderOnRequest();
+  public void testLegacyV1HeaderRejected() throws Exception {
+    // A request with only the legacy SolrAuth (v1) header and no SolrAuthV2 
header should be
+    // rejected, since PKI v1 support has been removed. Verify that the plugin 
never even consults
+    // the SolrAuth header — the rejection must be due to the absence of 
SolrAuthV2, not due to
+    // the v1 token being malformed.
+    HttpServletRequest legacyReq = mock(HttpServletRequest.class);
+    
when(legacyReq.getHeader(PKIAuthenticationPlugin.HEADER_V2)).thenReturn(null);
+    when(legacyReq.getRequestURI()).thenReturn("/collection1/select");
 
     HttpServletResponse response = mock(HttpServletResponse.class);
-    // This will fail in the same way that a missing header would fail
     assertFalse(
-        "Should have failed authentication", mock.authenticate(mockReq, 
response, filterChain));
+        "Should have rejected request with only a legacy v1 SolrAuth header",
+        mock.authenticate(legacyReq, response, filterChain));
 
+    verify(legacyReq, never()).getHeader("SolrAuth");
     verify(response)
         .setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), 
PKIAuthenticationPlugin.HEADER_V2);
     verify(response).sendError(ArgumentMatchers.eq(401), anyString());
@@ -220,89 +207,6 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
         "Should not have proceeded after authentication failure", 
wrappedRequestByFilter.get());
   }
 
-  public void testParseCipher() {
-    for (String validUser : new String[] {"user1", "$", "some user", "some 
123"}) {
-      for (long validTimestamp :
-          new long[] {Instant.now().toEpochMilli(), 99999999999L, 
9999999999999L}) {
-        String s = validUser + " " + validTimestamp;
-        byte[] payload = s.getBytes(UTF_8);
-        byte[] payloadCipher = aKeyPair.encrypt(ByteBuffer.wrap(payload));
-        String base64Cipher = 
Base64.getEncoder().encodeToString(payloadCipher);
-        PKIAuthenticationPlugin.PKIHeaderData header = 
parseCipher(base64Cipher);
-        assertNotNull(
-            "Expecting valid header for user " + validUser + " and timestamp " 
+ validTimestamp,
-            header);
-        assertEquals(validUser, header.userName);
-        assertEquals(validTimestamp, header.timestamp);
-      }
-    }
-  }
-
-  public void testParseCipherInvalidTimestampTooSmall() {
-    long timestamp = 999999999L;
-    String s = "user1 " + timestamp;
-
-    byte[] payload = s.getBytes(UTF_8);
-    byte[] payloadCipher = aKeyPair.encrypt(ByteBuffer.wrap(payload));
-    String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
-    assertNull(parseCipher(base64Cipher));
-  }
-
-  public void testParseCipherInvalidTimestampTooBig() {
-    long timestamp = 10000000000000L;
-    String s = "user1 " + timestamp;
-
-    byte[] payload = s.getBytes(UTF_8);
-    byte[] payloadCipher = aKeyPair.encrypt(ByteBuffer.wrap(payload));
-    String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
-    assertNull(parseCipher(base64Cipher));
-  }
-
-  public void testParseCipherInvalidKey() {
-    String s = "user1 " + Instant.now().toEpochMilli();
-    byte[] payload = s.getBytes(UTF_8);
-    byte[] payloadCipher = aKeyPair.encrypt(ByteBuffer.wrap(payload));
-    String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
-    assertNull(
-        PKIAuthenticationPlugin.parseCipher(
-            base64Cipher, new CryptoKeys.RSAKeyPair().getPublicKey(), true));
-  }
-
-  public void testParseCipherNoSpace() {
-    String s = "user1" + Instant.now().toEpochMilli(); // missing space
-
-    byte[] payload = s.getBytes(UTF_8);
-    byte[] payloadCipher = aKeyPair.encrypt(ByteBuffer.wrap(payload));
-    String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
-    assertNull(parseCipher(base64Cipher));
-  }
-
-  public void testParseCipherNoTimestamp() {
-    String s = "user1 aaaaaaaaaa";
-
-    byte[] payload = s.getBytes(UTF_8);
-    byte[] payloadCipher = aKeyPair.encrypt(ByteBuffer.wrap(payload));
-    String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
-    assertNull(parseCipher(base64Cipher));
-  }
-
-  private PKIAuthenticationPlugin.PKIHeaderData parseCipher(String 
base64Cipher) {
-    return PKIAuthenticationPlugin.parseCipher(base64Cipher, 
aKeyPair.getPublicKey(), true);
-  }
-
-  public void testParseCipherInvalidKeyExample() {
-    /*
-    This test shows a case with an invalid public key for which the decrypt 
will return an output that triggers SOLR-15961.
-     */
-    String base64Cipher =
-        
"A8tEkMfmA5m5+wVG9xSI46Lhg8MqDFkjPVqXc6Tf6LT/EVIpW3DUrkIygIjk9tSCCAxhHwSvKfVJeujaBtxr19ajmpWjtZKgZOXkynF5aPbDuI+mnvCiTmhLuZYExvnmeYxag6A4Fu2TpA/Wo97S4cIkRgfyag/ZOYM0pZwVAtNoJgTpmODDGrH4W16BXSZ6xm+EV4vrfUqpuuO7U7YiU5fd1tv22Au0ZaY6lPbxAHjeFyD8WrkPPIkEoM14K0G5vAg4wUxpRF/eVlnzhULoPgKFErz7cKVxuvxSsYpVw5oko+ldzyfsnMrC1brqUKA7NxhpdpJzp7bmd8W8/mvZEw==";
-    String publicKey =
-        
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJu1O+A/gGikFSeLGYdgNPrz3ef/tqJP1sRqzkVjnBcdyI2oXMmAWF+yDe0Zmya+HevyOI8YN2Yaq6aCLjbHnT364Rno/urhKvR5PmaH/PqXrh3Dl+vn08B74iLVZxZro/v34FGjX8fkiasZggC4AnyLjFkU7POsHhJKSXGslsWe0dq7yaaA2AES/bFwJ3r3FNxUsE+kWEtZG1RKMq8P8wlx/HLDzjYKaGnyApAltBHVx60XHiOC9Oatu5HZb/eKU3jf7sKibrzrRsqwb+iE4ZxxtXkgATuLOl/2ks5Mnkk4u7bPEAgEpEuzQBB4AahMC7r+R5AzRnB4+xx69FP1IwIDAQAB";
-    assertNull(
-        PKIAuthenticationPlugin.parseCipher(
-            base64Cipher, CryptoKeys.deserializeX509PublicKey(publicKey), 
true));
-  }
-
   private HttpServletRequest createMockRequest(final AtomicReference<String> 
headerValue) {
     HttpServletRequest mockReq = mock(HttpServletRequest.class);
     when(mockReq.getHeader(any(String.class)))
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/authentication-and-authorization-plugins.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/authentication-and-authorization-plugins.adoc
index 8aa32a3f702..61abd3fc8a7 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/authentication-and-authorization-plugins.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/authentication-and-authorization-plugins.adoc
@@ -211,23 +211,15 @@ They may do this through the so-called 
`HttpClientBuilder` mechanism, or they ma
 The `PKIAuthenticationPlugin` provides a built-in authentication mechanism 
where each Solr node is a super user and is fully trusted by other Solr nodes 
through the use of Public Key Infrastructure (PKI).
 Each Authentication plugin may choose to delegate all or some inter-node 
traffic to the PKI plugin.
 
-There are currently two versions of the PKI Authentication protocol available 
in Solr. For each outgoing request `PKIAuthenticationPlugin` adds a special 
header which carries the request timestamp and user principal.
-When a node receives a request with this special header, it will verify to 
message using the corresponding source node's public key.
+For each outgoing request `PKIAuthenticationPlugin` adds a `SolrAuthV2` header 
which contains: the source node name, user principal, request timestamp, and a 
base64-encoded RSA signature.
+When a node receives a request with this header, it will verify the message 
using the corresponding source node's public key.
 Message validation is only attempted for incoming traffic from other Solr 
nodes registered in ZooKeeper.
-If the request passes PKI validation and the timestamp is less than 5 seconds 
old, then the request will be trusted.
+If the request passes PKI validation and the timestamp is within the 
configured `pkiauth.ttl` time-to-live window, then the request will be trusted.
 
 [NOTE]
 ====
 Note: Because the PKI Authentication Plugin relies on relatively short 
timestamp expiration to validate requests, the clocks on separate nodes in the 
cluster must be synchronized.
 ====
 
-Version 2 of the protocol is the default version. In this version, the 
`SolrAuthV2` header contains: the source node name, user principal, request 
timestamp, and a base64-encoded RSA signature. All nodes will attempt to 
validate this header first.
-
-To support rolling restarts from older versions, Solr can be configured to 
accept and validate PKI authentication using protocol v1. This is enabled by 
setting the system properties `solr.pki.sendVersion=v1` and 
`solr.pki.acceptVersions=v1,v2`. When enabled, requests will contain a 
`SolrAuth` header which will contain the user principal and timestamp encrypted 
using the sender's private key.
-
-If the `SolrAuthV2` header is present but fails validation, then Solr will not 
fall back to checking `SolrAuth`. The legacy authentication headers will only 
be consulted when the newest headers are not present.
-
-Unknown values for `solr.pki.acceptVersion` will emit a warning log message 
but will not cause errors to more smoothly support future protocol revisions.
-
 The timeout is configurable through a system property called `pkiauth.ttl`.
 For example, if you wish to increase the time-to-live to 10 seconds (10,000 
milliseconds), start each node with a property `'-Dpkiauth.ttl=10000'`.
diff --git 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc
index 546f93f4b3a..375109ddbb7 100644
--- 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc
+++ 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc
@@ -53,6 +53,14 @@ The `blockUnknown` setting in the JWT Authentication plugin 
now defaults to `tru
 In Solr 10.0, the code default was `false` (pass-through), which contradicted 
the reference guide documentation that described `true` as the default.
 Users upgrading from 10.0 who relied on the pass-through behavior must 
explicitly set `"blockUnknown": false` in their `security.json`.
 
+=== Security
+
+PKI Authentication v1 support has been removed.
+Solr 10.1 nodes only send and accept the `SolrAuthV2` (v2) header for 
inter-node communication.
+
+Before performing a rolling upgrade to 10.1, ensure no node in the cluster has 
`solr.pki.sendVersion=v1` set, as those nodes would send the legacy `SolrAuth` 
header that 10.1 nodes will reject.
+The `solr.pki.sendVersion` and `solr.pki.acceptVersions` system properties are 
no longer recognized in 10.1 and can be removed from your configuration.
+
 == Solr 10.0
 
 === Solr Jetty parameters


Reply via email to