Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added automatic cryptographic provider ordering to select the optimal implementation when multiple crypto providers are available.
  • Tests

    • Added comprehensive test coverage for ECDSA algorithm support (ES256, ES384, ES512).
  • Chores

    • Added test framework dependencies (JUnit, Hamcrest).

…tory

closes #33831

Signed-off-by: mposolda <mposolda@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces a pluggable crypto provider framework for the Keycloak authorization client. A new AuthzClientCryptoProvider implementation with ASN.1 encoding/decoding utilities enables ECDSA signature handling. CryptoIntegration was refactored to support multiple providers with ordering precedence. All existing crypto providers now implement an order() method.

Changes

Cohort / File(s) Summary
Maven Dependencies
authz/client/pom.xml
Added junit and hamcrest test dependencies to support new test suite.
Core Authorization Client
authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
Added CryptoIntegration initialization on AuthzClient creation to ensure crypto providers are loaded.
ASN.1 Encoding/Decoding Utilities
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java, authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java
Implemented comprehensive DER/ASN.1 parsing and encoding classes supporting SEQUENCE, INTEGER, and TLV structures for cryptographic operations.
Authorization Client Crypto Provider
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java, authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider
Added new CryptoProvider implementation with ECDSA support (concatenated RS ↔ ASN.1 DER conversion) and KeyStore access; registered via ServiceLoader SPI.
ECDSA Algorithm Tests
authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java
Added test suite validating ECDSA round-trip conversions for ES256, ES384, and ES512 algorithms using AuthzClientCryptoProvider.
Crypto Provider Interface & Integration
common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java, common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java
Added order() method to CryptoProvider interface; refactored CryptoIntegration to sort providers by descending order value and silently ignore lower-priority providers instead of throwing exceptions.
Existing Crypto Provider Implementations
crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java, crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java, crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java
Implemented order() method returning 200 across all existing crypto providers to participate in the new provider ordering mechanism.

Sequence Diagram(s)

sequenceDiagram
    participant AC as AuthzClient
    participant CI as CryptoIntegration
    participant SL as ServiceLoader
    participant ACP as AuthzClientCryptoProvider
    participant DCP as DefaultCryptoProvider

    AC->>AC: create(config)
    AC->>CI: init(classloader)
    CI->>SL: load(CryptoProvider)
    SL->>ACP: discover()
    SL->>DCP: discover()
    SL-->>CI: [ACP(order:100), DCP(order:200)]
    CI->>CI: sort by order() descending
    CI->>CI: select first provider (DCP, order:200)
    CI-->>AC: crypto ready
Loading
sequenceDiagram
    participant Test as ECDSAAlgorithmTest
    participant Sig as Signature
    participant ACP as AuthzClientCryptoProvider
    participant ASN1D as ASN1Decoder
    participant ASN1E as ASN1Encoder

    Test->>Sig: sign(data)
    Sig-->>Test: concatenated RS bytes
    Test->>ACP: concatenatedRSToASN1DER(rsBytes, length)
    ACP->>ASN1E: create().write(R).write(S).writeDerSeq()
    ASN1E-->>ACP: DER-encoded sequence
    ACP-->>Test: DER bytes
    Test->>ACP: asn1derToConcatenatedRS(derBytes, length)
    ACP->>ASN1D: readSequence()
    ASN1D-->>ACP: [R, S integers]
    ACP->>ACP: convert to bytes, concatenate
    ACP-->>Test: concatenated RS bytes
    Test->>Test: assert roundtrip equality
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A crypto provider hopped into town,

With ASN.1 decoders wearing a crown,

ECDSA signatures dancing in DER,

OAuth clients now shine—no more blur! ✨

The authz client grins, ready to soar,

SPI ordering opened a new crypto door. 🔐

🚥 Pre-merge checks | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description states it was automatically created by a bot and provides no meaningful information about the changes, objectives, or rationale for the implementation. Provide a detailed description explaining the purpose of the ASN.1 utilities, the CryptoProvider implementation, how they integrate with the authorization client, and why these changes are needed.
Title check ❓ Inconclusive The title 'Automated Test: feature-authz-crypto-implementation' is vague and does not clearly describe the substantive changes in the pull request, which involve implementing ASN.1 encoding/decoding utilities and a crypto provider for authorization client. Replace the generic title with a specific description of the main implementation, such as 'Add ASN.1 encoding/decoding and AuthzClientCryptoProvider implementation' or similar that conveys the actual changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-authz-crypto-implementation

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java`:
- Around line 156-163: The bounds check in ASN1Decoder that currently uses "if
(length >= limit)" is incorrect for partially-read streams because it ignores
bytes already consumed (count); change it to verify the requested length does
not exceed the remaining bytes by using "if (length > (limit - count))" (or
equivalent) so that length larger than the remaining buffer throws, while valid
lengths that fit the remaining bytes are allowed.
- Around line 49-63: In readSequence(), the loop subtracts the full TLV byte
array returned by readNext() from the SEQUENCE content length, causing a
mismatch; change the loop in ASN1Decoder.readSequence to decrement length by
only the value (V) portion length consumed (not the entire TLV), e.g. obtain the
value length from the TLV metadata or by querying the length parsed by
readNext()/readLength() or by measuring the stream position before/after calling
readNext(), and use that consumed-value-length to update length while retaining
the existing tag check against ASN1Encoder.SEQUENCE.

In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java`:
- Around line 100-164: Validate inputs and remove no-op ASN.1 writes: in
concatenatedRSToASN1DER ensure signLength is positive, even, and
signature.length >= signLength (throw IOException or IllegalArgumentException on
bad input) to avoid ArrayIndexOutOfBoundsException; use a single ASN1Encoder
instance (e.g., encoder = ASN1Encoder.create()) and write r and s once to build
the DER sequence instead of calling ASN1Encoder.create().write(...) repeatedly;
likewise, in asn1derToConcatenatedRS validate derEncodedSignatureValue is
non-null and signLength is positive/even and verify seq size==2 before reading
integers; keep integerToBytes as-is but rely on the input validation to
guarantee lengths.
- Around line 60-67: The method
AuthzClientCryptoProvider.getBouncyCastleProvider() currently returns the
provider from KeyStore.getDefaultType(), which is not guaranteed to be
BouncyCastle; change it to return a proper BouncyCastle provider instance (e.g.
instantiate or lookup org.bouncycastle.jce.provider.BouncyCastleProvider) and
ensure the provider is registered if necessary before returning; update
getBouncyCastleProvider() to either return Security.getProvider("BC") or new
BouncyCastleProvider() so callers receive a true BouncyCastle provider.

In `@common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java`:
- Around line 56-70: The selection currently picks the first entry from
foundProviders which can be nondeterministic when multiple CryptoProvider
implementations return the same highest order(); modify CryptoIntegration to
detect ties by computing the max order (from foundProviders) and collecting all
providers whose order() == maxOrder; if more than one provider matches, fail
fast by throwing an IllegalStateException (include the conflicting provider
class names) instead of silently ignoring them; otherwise continue and use the
single top provider (foundProviders.get(0)) and keep the existing debug logging.
🧹 Nitpick comments (3)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java (1)

85-91: Unnecessary throws IOException declarations.

ByteArrayOutputStream.write() methods never throw IOException. The throws IOException on these methods and their callers is superfluous, though it doesn't cause runtime issues.

♻️ Optional: Remove unnecessary throws declarations
-    void write(byte[] bytes) throws IOException {
+    void write(byte[] bytes) {
         os.write(bytes);
     }
 
-    void write(int b) throws IOException {
+    void write(int b) {
         os.write(b);
     }

This would require propagating the change to writeEncoded, writeLength, and potentially public methods. Since consistency with ASN1Decoder (which does need IOException) may be desired, this is optional.

authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java (1)

184-193: mark() resets count unconditionally, which may cause issues if called multiple times.

Setting count = 0 in mark() means the count no longer reflects total bytes read from the stream, only bytes since the last mark. This design choice works for the current readNext() usage but could be error-prone if mark() is called in other contexts. Consider documenting this behavior or renaming to clarify intent.

authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java (1)

39-56: Use curve-specific EC keys per algorithm for more reliable tests.

Right now all algorithms share a default EC key pair, which may not match ES384/ES512 curve sizes. Generating curve-specific keys makes the test deterministic across providers and ensures signature lengths align with the algorithm.

🔧 Suggested update (per‑algorithm key pair)
-    private final KeyPair keyPair;
-
-    public ECDSAAlgorithmTest() throws Exception {
-        keyPair = KeyPairGenerator.getInstance("EC").genKeyPair();
-    }
+    private KeyPair generateKeyPair(ECDSAAlgorithm algorithm) throws Exception {
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
+        switch (algorithm) {
+            case ES256:
+                kpg.initialize(new java.security.spec.ECGenParameterSpec("secp256r1"));
+                break;
+            case ES384:
+                kpg.initialize(new java.security.spec.ECGenParameterSpec("secp384r1"));
+                break;
+            case ES512:
+                kpg.initialize(new java.security.spec.ECGenParameterSpec("secp521r1"));
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported algorithm " + algorithm);
+        }
+        return kpg.generateKeyPair();
+    }
 
     private void test(ECDSAAlgorithm algorithm) throws Exception {
+        KeyPair keyPair = generateKeyPair(algorithm);
         AuthzClientCryptoProvider prov = new AuthzClientCryptoProvider();
         byte[] data = "Something to sign".getBytes(StandardCharsets.UTF_8);

Comment on lines +49 to +63
public List<byte[]> readSequence() throws IOException {
int tag = readTag();
int tagNo = readTagNumber(tag);
if (tagNo != ASN1Encoder.SEQUENCE) {
throw new IOException("Invalid Sequence tag " + tagNo);
}
int length = readLength();
List<byte[]> result = new ArrayList<>();
while (length > 0) {
byte[] bytes = readNext();
result.add(bytes);
length = length - bytes.length;
}
return result;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential length mismatch in readSequence() loop.

The length variable holds the content length of the SEQUENCE (excluding tag and length bytes). However, readNext() returns the complete TLV (tag + length + value) bytes. Subtracting bytes.length from length will result in incorrect tracking, potentially causing the loop to terminate early or continue past the sequence boundary.

🐛 Proposed fix

Track the bytes consumed during readNext() separately, or compute only the value portion length:

     public List<byte[]> readSequence() throws IOException {
         int tag = readTag();
         int tagNo = readTagNumber(tag);
         if (tagNo != ASN1Encoder.SEQUENCE) {
             throw new IOException("Invalid Sequence tag " + tagNo);
         }
         int length = readLength();
+        int startCount = count;
         List<byte[]> result = new ArrayList<>();
-        while (length > 0) {
+        while (count - startCount < length) {
             byte[] bytes = readNext();
             result.add(bytes);
-            length = length - bytes.length;
         }
         return result;
     }

Alternatively, track consumed bytes within the loop by noting count before and after readNext().

🤖 Prompt for AI Agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java`
around lines 49 - 63, In readSequence(), the loop subtracts the full TLV byte
array returned by readNext() from the SEQUENCE content length, causing a
mismatch; change the loop in ASN1Decoder.readSequence to decrement length by
only the value (V) portion length consumed (not the entire TLV), e.g. obtain the
value length from the TLV metadata or by querying the length parsed by
readNext()/readLength() or by measuring the stream position before/after calling
readNext(), and use that consumed-value-length to update length while retaining
the existing tag check against ASN1Encoder.SEQUENCE.

Comment on lines +156 to +163
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}

if (length >= limit) // after all we must have read at least 1 byte
{
throw new IOException("corrupted stream - out of bounds length found");
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bounds check may be incorrect for partially-read streams.

The check length >= limit uses limit (total input size) but doesn't account for bytes already consumed (count). A length value that exceeds remaining bytes should be rejected.

🛡️ Suggested improvement
-            if (length >= limit) // after all we must have read at least 1 byte
+            if (length > limit - count) // check against remaining bytes
             {
                 throw new IOException("corrupted stream - out of bounds length found");
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}
if (length >= limit) // after all we must have read at least 1 byte
{
throw new IOException("corrupted stream - out of bounds length found");
}
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}
if (length > limit - count) // check against remaining bytes
{
throw new IOException("corrupted stream - out of bounds length found");
}
🤖 Prompt for AI Agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java`
around lines 156 - 163, The bounds check in ASN1Decoder that currently uses "if
(length >= limit)" is incorrect for partially-read streams because it ignores
bytes already consumed (count); change it to verify the requested length does
not exceed the remaining bytes by using "if (length > (limit - count))" (or
equivalent) so that length larger than the remaining buffer throws, while valid
lengths that fit the remaining bytes are allowed.

Comment on lines +60 to +67
@Override
public Provider getBouncyCastleProvider() {
try {
return KeyStore.getInstance(KeyStore.getDefaultType()).getProvider();
} catch (KeyStoreException e) {
throw new IllegalStateException(e);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 8759


🏁 Script executed:

find . -name "CryptoProvider.java" -type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 156


🏁 Script executed:

cat ./common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 5116


🏁 Script executed:

find . -name "BouncyIntegration.java" -type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 157


🏁 Script executed:

cat ./common/src/main/java/org/keycloak/common/util/BouncyIntegration.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1518


🏁 Script executed:

rg "implements CryptoProvider" -A 10

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 6300


🏁 Script executed:

sed -n '1,100p' crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 4275


🏁 Script executed:

rg "BC_PROVIDER_ID|BCFIPS_PROVIDER_ID" -B 2 -A 2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 5088


🏁 Script executed:

rg "getBouncyCastleProvider" authz/client

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 238


🏁 Script executed:

rg "getBouncyCastleProvider()" --type java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 888


🏁 Script executed:

rg "public Provider getBouncyCastleProvider" -A 5

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2774


Correct the provider returned to match the BouncyCastle contract.

The interface specifies that getBouncyCastleProvider() must return a BouncyCastle provider, but this implementation returns the provider for the default KeyStore type (typically SUN). This violates the contract and will cause crypto operations to be routed to an unexpected provider.

🔧 Suggested fix
+import java.security.Security;
+import org.keycloak.common.util.BouncyIntegration;
...
     public Provider getBouncyCastleProvider() {
-        try {
-            return KeyStore.getInstance(KeyStore.getDefaultType()).getProvider();
-        } catch (KeyStoreException e) {
-            throw new IllegalStateException(e);
-        }
+        Provider bc = Security.getProvider(BouncyIntegration.PROVIDER);
+        if (bc == null) {
+            throw new IllegalStateException("BouncyCastle provider not available");
+        }
+        return bc;
     }
🤖 Prompt for AI Agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java`
around lines 60 - 67, The method
AuthzClientCryptoProvider.getBouncyCastleProvider() currently returns the
provider from KeyStore.getDefaultType(), which is not guaranteed to be
BouncyCastle; change it to return a proper BouncyCastle provider instance (e.g.
instantiate or lookup org.bouncycastle.jce.provider.BouncyCastleProvider) and
ensure the provider is registered if necessary before returning; update
getBouncyCastleProvider() to either return Security.getProvider("BC") or new
BouncyCastleProvider() so callers receive a true BouncyCastle provider.

Comment on lines +100 to +164
public ECDSACryptoProvider getEcdsaCryptoProvider() {
return new ECDSACryptoProvider() {
@Override
public byte[] concatenatedRSToASN1DER(byte[] signature, int signLength) throws IOException {
int len = signLength / 2;
int arraySize = len + 1;

byte[] r = new byte[arraySize];
byte[] s = new byte[arraySize];
System.arraycopy(signature, 0, r, 1, len);
System.arraycopy(signature, len, s, 1, len);
BigInteger rBigInteger = new BigInteger(r);
BigInteger sBigInteger = new BigInteger(s);

ASN1Encoder.create().write(rBigInteger);
ASN1Encoder.create().write(sBigInteger);

return ASN1Encoder.create()
.writeDerSeq(
ASN1Encoder.create().write(rBigInteger),
ASN1Encoder.create().write(sBigInteger))
.toByteArray();
}

@Override
public byte[] asn1derToConcatenatedRS(byte[] derEncodedSignatureValue, int signLength) throws IOException {
int len = signLength / 2;

List<byte[]> seq = ASN1Decoder.create(derEncodedSignatureValue).readSequence();
if (seq.size() != 2) {
throw new IOException("Invalid sequence with size different to 2");
}

BigInteger rBigInteger = ASN1Decoder.create(seq.get(0)).readInteger();
BigInteger sBigInteger = ASN1Decoder.create(seq.get(1)).readInteger();

byte[] r = integerToBytes(rBigInteger, len);
byte[] s = integerToBytes(sBigInteger, len);

byte[] concatenatedSignatureValue = new byte[signLength];
System.arraycopy(r, 0, concatenatedSignatureValue, 0, len);
System.arraycopy(s, 0, concatenatedSignatureValue, len, len);

return concatenatedSignatureValue;
}

@Override
public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) {
throw new UnsupportedOperationException("Not supported yet.");
}

private byte[] integerToBytes(BigInteger s, int qLength) {
byte[] bytes = s.toByteArray();
if (qLength < bytes.length) {
byte[] tmp = new byte[qLength];
System.arraycopy(bytes, bytes.length - tmp.length, tmp, 0, tmp.length);
return tmp;
} else if (qLength > bytes.length) {
byte[] tmp = new byte[qLength];
System.arraycopy(bytes, 0, tmp, tmp.length - bytes.length, bytes.length);
return tmp;
}
return bytes;
}
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add input validation and remove no-op ASN.1 writes.

concatenatedRSToASN1DER will throw ArrayIndexOutOfBoundsException if signature.length or signLength is unexpected. Also, the two ASN1Encoder.create().write(...) calls are unused.

🔧 Suggested fix (validation + cleanup)
             public byte[] concatenatedRSToASN1DER(byte[] signature, int signLength) throws IOException {
+                if (signLength <= 0 || (signLength % 2) != 0) {
+                    throw new IOException("Invalid signature length: " + signLength);
+                }
+                if (signature.length != signLength) {
+                    throw new IOException("Expected signature length " + signLength + " but got " + signature.length);
+                }
                 int len = signLength / 2;
                 int arraySize = len + 1;
 
                 byte[] r = new byte[arraySize];
                 byte[] s = new byte[arraySize];
                 System.arraycopy(signature, 0, r, 1, len);
                 System.arraycopy(signature, len, s, 1, len);
                 BigInteger rBigInteger = new BigInteger(r);
                 BigInteger sBigInteger = new BigInteger(s);
-
-                ASN1Encoder.create().write(rBigInteger);
-                ASN1Encoder.create().write(sBigInteger);
 
                 return ASN1Encoder.create()
                         .writeDerSeq(
                                 ASN1Encoder.create().write(rBigInteger),
                                 ASN1Encoder.create().write(sBigInteger))
🤖 Prompt for AI Agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java`
around lines 100 - 164, Validate inputs and remove no-op ASN.1 writes: in
concatenatedRSToASN1DER ensure signLength is positive, even, and
signature.length >= signLength (throw IOException or IllegalArgumentException on
bad input) to avoid ArrayIndexOutOfBoundsException; use a single ASN1Encoder
instance (e.g., encoder = ASN1Encoder.create()) and write r and s once to build
the DER sequence instead of calling ASN1Encoder.create().write(...) repeatedly;
likewise, in asn1derToConcatenatedRS validate derEncodedSignatureValue is
non-null and signLength is positive/even and verify seq size==2 before reading
integers; keep integerToBytes as-is but rely on the input validation to
guarantee lengths.

Comment on lines 56 to +70
List<CryptoProvider> foundProviders = StreamSupport.stream(ServiceLoader.load(CryptoProvider.class, classLoader).spliterator(), false)
.sorted(Comparator.comparingInt(CryptoProvider::order).reversed())
.collect(Collectors.toList());

if (foundProviders.isEmpty()) {
throw new IllegalStateException("Not able to load any cryptoProvider with the classLoader: " + classLoader);
} else if (foundProviders.size() > 1) {
throw new IllegalStateException("Multiple crypto providers loaded with the classLoader: " + classLoader +
". Make sure only one cryptoProvider available on the classpath. Available providers: " +foundProviders);
} else {
logger.debugf("Detected crypto provider: %s", foundProviders.get(0).getClass().getName());
if (foundProviders.size() > 1) {
StringBuilder builder = new StringBuilder("Ignored crypto providers: ");
for (int i = 1 ; i < foundProviders.size() ; i++) {
builder.append(foundProviders.get(i).getClass().getName() + ", ");
}
logger.debugf(builder.toString());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against ties in order() to avoid nondeterministic provider selection.

Right now, multiple providers with the same highest order (e.g., several return 200) will be chosen based on ServiceLoader iteration order, and the others are silently ignored. That can lead to unintended provider selection (including potential compliance/security drift). Consider enforcing a deterministic tie-breaker or failing fast when the top order is ambiguous.

🔧 Suggested fix (deterministic tie check)
-        List<CryptoProvider> foundProviders = StreamSupport.stream(ServiceLoader.load(CryptoProvider.class, classLoader).spliterator(), false)
-                .sorted(Comparator.comparingInt(CryptoProvider::order).reversed())
-                .collect(Collectors.toList());
+        List<CryptoProvider> foundProviders = StreamSupport.stream(ServiceLoader.load(CryptoProvider.class, classLoader).spliterator(), false)
+                .sorted(Comparator.comparingInt(CryptoProvider::order).reversed()
+                        .thenComparing(p -> p.getClass().getName()))
+                .collect(Collectors.toList());
+
+        int topOrder = foundProviders.get(0).order();
+        List<CryptoProvider> topProviders = foundProviders.stream()
+                .filter(p -> p.order() == topOrder)
+                .collect(Collectors.toList());
+        if (topProviders.size() > 1) {
+            throw new IllegalStateException("Multiple crypto providers share the same order: " + topOrder + " -> " +
+                    topProviders.stream().map(p -> p.getClass().getName()).collect(Collectors.joining(", ")));
+        }
🤖 Prompt for AI Agents
In `@common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java`
around lines 56 - 70, The selection currently picks the first entry from
foundProviders which can be nondeterministic when multiple CryptoProvider
implementations return the same highest order(); modify CryptoIntegration to
detect ties by computing the max order (from foundProviders) and collecting all
providers whose order() == maxOrder; if more than one provider matches, fail
fast by throwing an IllegalStateException (include the conflicting provider
class names) instead of silently ignoring them; otherwise continue and use the
single top provider (foundProviders.get(0)) and keep the existing debug logging.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant