Skip to content

Commit ef3d1a2

Browse files
authored
SOLR-18235 Remove support for old PKI auth v1 (#4405)
1 parent b1c386f commit ef3d1a2

8 files changed

Lines changed: 53 additions & 381 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
title: Remove support for PKI Authentication v1. The system properties solr.pki.sendVersion and solr.pki.acceptVersions are no longer recognized.
2+
type: removed
3+
authors:
4+
- name: Jan Høydahl
5+
links:
6+
- name: SOLR-18235
7+
url: https://issues.apache.org/jira/browse/SOLR-18235

solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java

Lines changed: 21 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,14 @@
2727
import jakarta.servlet.http.HttpServletResponse;
2828
import java.io.IOException;
2929
import java.lang.invoke.MethodHandles;
30-
import java.nio.ByteBuffer;
3130
import java.security.InvalidKeyException;
3231
import java.security.Principal;
3332
import java.security.PublicKey;
3433
import java.security.SignatureException;
3534
import java.time.Instant;
3635
import java.util.Base64;
37-
import java.util.List;
3836
import java.util.Map;
3937
import java.util.Optional;
40-
import java.util.Set;
4138
import java.util.concurrent.ConcurrentHashMap;
4239
import java.util.concurrent.TimeUnit;
4340
import java.util.function.BiConsumer;
@@ -47,7 +44,6 @@
4744
import org.apache.solr.common.params.ModifiableSolrParams;
4845
import org.apache.solr.common.util.ExecutorUtil;
4946
import org.apache.solr.common.util.NamedList;
50-
import org.apache.solr.common.util.StrUtils;
5147
import org.apache.solr.common.util.SuppressForbidden;
5248
import org.apache.solr.core.CoreContainer;
5349
import org.apache.solr.request.SolrRequestInfo;
@@ -60,9 +56,6 @@
6056
public class PKIAuthenticationPlugin extends AuthenticationPlugin
6157
implements HttpClientBuilderPlugin {
6258

63-
public static final String ACCEPT_VERSIONS = "solr.pki.acceptVersions";
64-
public static final String SEND_VERSION = "solr.pki.sendVersion";
65-
6659
/**
6760
* Mark the current thread as a server thread and set a flag in SolrRequestInfo to indicate you
6861
* want to send a request as the server identity instead of as the authenticated user.
@@ -80,24 +73,15 @@ public static void withServerIdentity(final boolean enabled) {
8073

8174
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
8275

83-
/** If a number has less than this number of digits, it'll not be considered a timestamp. */
84-
private static final int MIN_TIMESTAMP_DIGITS = 10; // a timestamp of 9999999999 is year 1970
85-
86-
/** If a number has more than this number of digits, it'll not be considered a timestamp. */
87-
private static final int MAX_TIMESTAMP_DIGITS = 13; // a timestamp of 9999999999999 is year 2286
88-
8976
private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
9077
private final PublicKeyHandler publicKeyHandler;
9178
private final CoreContainer cores;
9279
private final LoadingCache<String, PKIHeaderData> validatedHeaderCache;
93-
private final LoadingCache<String, String> generatedV1TokenCache;
9480
private final LoadingCache<String, String> generatedV2TokenCache;
9581
private static final int MAX_VALIDITY = Integer.getInteger("pkiauth.ttl", 10000);
9682
private final String myNodeName;
9783
private boolean interceptorRegistered = false;
9884

99-
private boolean acceptPkiV1 = false;
100-
10185
public boolean isInterceptorRegistered() {
10286
return interceptorRegistered;
10387
}
@@ -130,34 +114,12 @@ public PKIAuthenticationPlugin(
130114
// runway for requests to come in to trigger an asynchronous-refresh before expiry causes a
131115
// synchronous-refresh.
132116
long shouldRefreshTime = Math.max(1, expireAfterTime / 2);
133-
generatedV1TokenCache =
134-
Caffeine.newBuilder()
135-
.maximumSize(100)
136-
.refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
137-
.expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
138-
.build(this::generateToken);
139117
generatedV2TokenCache =
140118
Caffeine.newBuilder()
141119
.maximumSize(100)
142120
.refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
143121
.expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
144122
.build(this::generateTokenV2);
145-
146-
Set<String> knownPkiVersions = Set.of("v1", "v2");
147-
// We always accept v2 even if it is not specified
148-
String[] versions = System.getProperty(ACCEPT_VERSIONS, "v2").split(",");
149-
for (String version : versions) {
150-
if (knownPkiVersions.contains(version) == false) {
151-
log.warn("Unknown protocol version [{}] specified in {}", version, ACCEPT_VERSIONS);
152-
}
153-
if ("v1".equals(version)) {
154-
log.warn(
155-
"System setting {} includes the deprecated v1, which should only be used for compatibility during rolling upgrades. "
156-
+ "After all servers have been upgraded, consider disabling this compatability layer.",
157-
ACCEPT_VERSIONS);
158-
acceptPkiV1 = true;
159-
}
160-
}
161123
}
162124

163125
@Override
@@ -171,35 +133,25 @@ public boolean doAuthenticate(
171133
// Getting the received time must be the first thing we do, processing the request can take time
172134
long receivedTime = System.currentTimeMillis();
173135

174-
PKIHeaderData headerData = null;
175136
String headerV2 = request.getHeader(HEADER_V2);
176-
String headerV1 = request.getHeader(HEADER);
177-
if (headerV1 == null && headerV2 == null) {
178-
return sendError(response, true, "No PKI auth header was provided");
179-
} else if (headerV2 != null) {
180-
// Try V2 first
181-
int nodeNameEnd = headerV2.indexOf(' ');
182-
if (nodeNameEnd <= 0) {
183-
// Do not log the value as it is likely gibberish
184-
return sendError(response, true, "Could not parse node name from SolrAuthV2 header.");
185-
}
137+
if (headerV2 == null) {
138+
return sendError(response, "No PKI auth header was provided");
139+
}
186140

187-
headerData = validatedHeaderCache.get(headerV2);
188-
} else if (headerV1 != null && acceptPkiV1) {
189-
List<String> authInfo = StrUtils.splitWS(headerV1, false);
190-
if (authInfo.size() != 2) {
191-
// We really shouldn't be logging and returning this, but we did it before so keep that
192-
return sendError(response, false, "Invalid SolrAuth header: " + headerV1);
193-
}
194-
headerData = decipherHeader(authInfo.get(0), authInfo.get(1));
141+
int nodeNameEnd = headerV2.indexOf(' ');
142+
if (nodeNameEnd <= 0) {
143+
// Do not log the value as it is likely gibberish
144+
return sendError(response, "Could not parse node name from SolrAuthV2 header.");
195145
}
196146

147+
PKIHeaderData headerData = validatedHeaderCache.get(headerV2);
148+
197149
if (headerData == null) {
198-
return sendError(response, true, "Could not validate PKI header.");
150+
return sendError(response, "Could not validate PKI header.");
199151
}
200152
long elapsed = receivedTime - headerData.timestamp;
201153
if (elapsed > MAX_VALIDITY) {
202-
return sendError(response, true, "Expired key request timestamp, elapsed=" + elapsed);
154+
return sendError(response, "Expired key request timestamp, elapsed=" + elapsed);
203155
}
204156

205157
final Principal principal =
@@ -217,16 +169,14 @@ public boolean doAuthenticate(
217169
* authentication
218170
*
219171
* @param response the response to set error status with
220-
* @param v2 whether this authentication used the v1 or v2 header (true if v2)
221-
* @param message the message to log and send back to client. do not include anyhting sensitive
172+
* @param message the message to log and send back to client. do not include anything sensitive
222173
* here about server state
223174
* @return false to chain with calls from authenticate
224175
*/
225-
private boolean sendError(HttpServletResponse response, boolean v2, String message)
226-
throws IOException {
176+
private boolean sendError(HttpServletResponse response, String message) throws IOException {
227177
numErrors.inc();
228178
log.error(message);
229-
response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), v2 ? HEADER_V2 : HEADER);
179+
response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), HEADER_V2);
230180
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);
231181
return false;
232182
}
@@ -304,53 +254,6 @@ private PKIHeaderData validateSignature(String data, byte[] sig, PublicKey key,
304254
}
305255
}
306256

307-
private PKIHeaderData decipherHeader(String nodeName, String cipherBase64) {
308-
PublicKey key = getOrFetchPublicKey(nodeName);
309-
310-
PKIHeaderData header = parseCipher(cipherBase64, key, false);
311-
if (header == null) {
312-
log.warn("Failed to decrypt header, trying after refreshing the key ");
313-
key = fetchPublicKeyFromRemote(nodeName);
314-
return parseCipher(cipherBase64, key, true);
315-
} else {
316-
return header;
317-
}
318-
}
319-
320-
@VisibleForTesting
321-
static PKIHeaderData parseCipher(String cipher, PublicKey key, boolean isRetry) {
322-
byte[] bytes;
323-
try {
324-
bytes = CryptoKeys.decryptRSA(Base64.getDecoder().decode(cipher), key);
325-
} catch (Exception e) {
326-
if (isRetry) {
327-
log.error("Decryption failed on retry, key must be wrong", e);
328-
} else {
329-
log.info("Decryption failed on first attempt, will retry", e);
330-
}
331-
return null;
332-
}
333-
String s = new String(bytes, UTF_8).trim();
334-
int splitPoint = s.lastIndexOf(' ');
335-
int timestampDigits = s.length() - 1 - splitPoint;
336-
if (splitPoint == -1
337-
|| timestampDigits < MIN_TIMESTAMP_DIGITS
338-
|| timestampDigits > MAX_TIMESTAMP_DIGITS) {
339-
log.warn("Invalid cipher {} deciphered data {}", cipher, s);
340-
return null;
341-
}
342-
PKIHeaderData headerData = new PKIHeaderData();
343-
try {
344-
headerData.timestamp = Long.parseLong(s.substring(splitPoint + 1));
345-
headerData.userName = s.substring(0, splitPoint);
346-
log.debug("Successfully decrypted header {} {}", headerData.userName, headerData.timestamp);
347-
return headerData;
348-
} catch (NumberFormatException e) {
349-
log.warn("Invalid cipher {}", cipher);
350-
return null;
351-
}
352-
}
353-
354257
private boolean isInLiveNodes(String nodeName) {
355258
return cores
356259
.getZkController()
@@ -439,16 +342,10 @@ public void onBegin(Request request) {
439342
log.trace("onBegin: {}", request);
440343

441344
final Optional<String> preFetchedUser = getUserFromJettyRequest(request);
442-
if ("v1".equals(System.getProperty(SEND_VERSION))) {
443-
preFetchedUser
444-
.map(generatedV1TokenCache::get)
445-
.ifPresent(token -> request.headers(httpFields -> httpFields.add(HEADER, token)));
446-
} else {
447-
preFetchedUser
448-
.map(generatedV2TokenCache::get)
449-
.ifPresent(
450-
token -> request.headers(httpFields -> httpFields.add(HEADER_V2, token)));
451-
}
345+
preFetchedUser
346+
.map(generatedV2TokenCache::get)
347+
.ifPresent(
348+
token -> request.headers(httpFields -> httpFields.add(HEADER_V2, token)));
452349
}
453350

454351
private void cachePreFetchedUserOnJettyRequest(Request request) {
@@ -492,17 +389,6 @@ private Optional<String> getUser() {
492389
}
493390
}
494391

495-
@SuppressForbidden(reason = "Needs currentTimeMillis to set current time in header")
496-
private String generateToken(String usr) {
497-
assert usr != null;
498-
String s = usr + " " + System.currentTimeMillis();
499-
byte[] payload = s.getBytes(UTF_8);
500-
byte[] payloadCipher = publicKeyHandler.getKeyPair().encrypt(ByteBuffer.wrap(payload));
501-
String base64Cipher = Base64.getEncoder().encodeToString(payloadCipher);
502-
log.trace("generateToken: usr={} token={}", usr, base64Cipher);
503-
return myNodeName + " " + base64Cipher;
504-
}
505-
506392
private String generateTokenV2(String user) {
507393
assert user != null;
508394
String s = myNodeName + " " + user + " " + Instant.now().toEpochMilli();
@@ -515,15 +401,9 @@ private String generateTokenV2(String user) {
515401

516402
@VisibleForTesting
517403
void setHeader(BiConsumer<String, String> httpRequest) {
518-
if ("v1".equals(System.getProperty(SEND_VERSION))) {
519-
getUser()
520-
.map(generatedV1TokenCache::get)
521-
.ifPresent(token -> httpRequest.accept(HEADER, token));
522-
} else {
523-
getUser()
524-
.map(generatedV2TokenCache::get)
525-
.ifPresent(token -> httpRequest.accept(HEADER_V2, token));
526-
}
404+
getUser()
405+
.map(generatedV2TokenCache::get)
406+
.ifPresent(token -> httpRequest.accept(HEADER_V2, token));
527407
}
528408

529409
boolean isSolrThread() {
@@ -545,7 +425,6 @@ public String getPublicKey() {
545425
return publicKeyHandler.getKeyPair().getPublicKeyStr();
546426
}
547427

548-
public static final String HEADER = "SolrAuth";
549428
public static final String HEADER_V2 = "SolrAuthV2";
550429
public static final String NODE_IS_USER = "$";
551430
// special principal to denote the cluster member

solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,8 @@ private static boolean isAdminUI(String requestPath) {
147147
}
148148

149149
private boolean isInternodePKI(HttpServletRequest req, CoreContainer cores) {
150-
String header = req.getHeader(PKIAuthenticationPlugin.HEADER);
151150
String headerV2 = req.getHeader(PKIAuthenticationPlugin.HEADER_V2);
152-
return (header != null || headerV2 != null)
153-
&& cores.getPkiAuthenticationSecurityBuilder() != null;
151+
return headerV2 != null && cores.getPkiAuthenticationSecurityBuilder() != null;
154152
}
155153

156154
private void logAuthAttempt(HttpServletRequest req) {

0 commit comments

Comments
 (0)