Skip to content

Add AuthzClientCryptoProvider for authorization client cryptographic operations#3

Open
ShashankFC wants to merge 1 commit into
feature-authz-crypto-baselinefrom
feature-authz-crypto-implementation
Open

Add AuthzClientCryptoProvider for authorization client cryptographic operations#3
ShashankFC wants to merge 1 commit into
feature-authz-crypto-baselinefrom
feature-authz-crypto-implementation

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Test 3

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced cryptographic support with ECDSA algorithm implementation for authorization client operations.
    • Improved crypto provider selection with priority ordering mechanism.
  • Tests

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

    • Added testing framework dependencies.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/keycloak-coderabbit#3

…tory

closes #33831

Signed-off-by: mposolda <mposolda@gmail.com>
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 9, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes introduce cryptographic support into the authorization client by implementing a new CryptoProvider with ECDSA signature conversion utilities. The provider framework is extended with an ordering mechanism to support multiple providers coexisting. Existing crypto providers are updated to implement provider ordering, and the discovery logic is modified to select the highest-priority provider.

Changes

Cohort / File(s) Summary
Build Configuration
authz/client/pom.xml
Added test dependencies: junit and org.hamcrest:hamcrest with test scope.
Crypto Utilities
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java, authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java
New ASN.1 DER encoder and decoder classes for building and parsing ASN.1 structures; supports SEQUENCE, INTEGER tags, long-form length encoding, and TLV streaming.
Crypto Provider Implementation
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java
New AuthzClientCryptoProvider implementing CryptoProvider with ECDSA signature conversion between DER and concatenated RS formats; includes BouncyCastle provider wiring and KeyStore support.
Service Provider Registration
authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider
Registered AuthzClientCryptoProvider as a CryptoProvider service implementation for runtime discovery via ServiceLoader.
Client Integration
authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
Added CryptoIntegration.init() call during client construction to initialize crypto framework.
CryptoProvider Framework
common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java
Added order() method to CryptoProvider interface to specify provider precedence (higher values = higher priority).
Provider Discovery & Ordering
common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java
Modified to sort discovered providers by order() in descending order and log ignored providers instead of throwing exception when multiple providers exist.
Existing 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
Added order() override returning 200 in each provider to implement new ordering contract.
Test Coverage
authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java
New JUnit test class exercising ECDSA signature conversion (ES256, ES384, ES512) via DER ↔ concatenated RS transformations.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant AuthzClient
    participant CryptoIntegration
    participant ServiceLoader
    participant AuthzClientCryptoProvider
    participant DefaultCryptoProvider

    Client->>AuthzClient: create(config)
    AuthzClient->>CryptoIntegration: init()
    CryptoIntegration->>ServiceLoader: load CryptoProvider instances
    ServiceLoader->>AuthzClientCryptoProvider: discover
    ServiceLoader->>DefaultCryptoProvider: discover
    CryptoIntegration->>CryptoIntegration: sort by order() descending
    Note over CryptoIntegration: AuthzClientCryptoProvider(100) < DefaultCryptoProvider(200)
    CryptoIntegration->>CryptoIntegration: select DefaultCryptoProvider (highest order)
    CryptoIntegration-->>AuthzClient: provider configured
    AuthzClient-->>Client: ready
Loading
sequenceDiagram
    participant Test
    participant KeyPairGen
    participant Signature
    participant AuthzClientCryptoProvider
    participant ASN1Decoder
    participant ASN1Encoder

    Test->>KeyPairGen: generate EC key pair
    KeyPairGen-->>Test: ECPublicKey, ECPrivateKey
    Test->>Signature: sign(data) with private key
    Signature-->>Test: DER-encoded signature
    Test->>AuthzClientCryptoProvider: asn1derToConcatenatedRS(derSig)
    AuthzClientCryptoProvider->>ASN1Decoder: decode DER sequence
    ASN1Decoder-->>AuthzClientCryptoProvider: R, S as BigInteger
    AuthzClientCryptoProvider-->>Test: concatenated RS bytes
    Test->>AuthzClientCryptoProvider: concatenatedRSToASN1DER(rsBytes)
    AuthzClientCryptoProvider->>ASN1Encoder: encode R, S sequence
    ASN1Encoder-->>AuthzClientCryptoProvider: DER bytes
    AuthzClientCryptoProvider-->>Test: DER signature
    Test->>Signature: verify(data, derSig)
    Signature-->>Test: valid
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A crypto provider hops into the fold,
With ASN.1 dancers, DER stories told,
ECDSA signatures twist left and right,
Ordering providers with newfound light! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and provides only 'Test 3' with no meaningful details about the changes, objectives, or rationale for the implementation despite the changeset being substantial. Expand the description to explain what AuthzClientCryptoProvider does, why it was added, which cryptographic operations are supported, and any relevant context from issue #33831.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding AuthzClientCryptoProvider for the authorization client's cryptographic operations, which aligns with the substantial crypto utility implementations and provider additions in the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 93-125: In readTagNumber(int tag) the loop builds tagNo by ORing
and then left-shifting which can overflow an int for maliciously large high-tag
encodings; fix by adding overflow detection before each left shift (or use a
wider accumulator) and throw an IOException if the next shift/OR would exceed
int capacity. Concretely, in readTagNumber replace the blind "tagNo |= (b &
0x7f); tagNo <<= 7;" sequence with logic that either accumulates into a long and
validates it fits into int before assigning back to tagNo, or checks whether
(tagNo >>> (32 - 7)) != 0 (or equivalent) and throws "corrupted stream - tag
number too large" to prevent integer wraparound while parsing.
- Around line 49-63: readSequence currently assumes definite-length; call
readLength() and if it returns -1 (indefinite-length) then explicitly reject it
by throwing an IOException (e.g. "Indefinite-length SEQUENCE not supported") or,
if BER support is required, implement an indefinite-length branch that
repeatedly reads elements (using readNext()/readTag()/readLength() as needed)
until an EOC (tag 0 and length 0) is encountered; update the while loop in
readSequence to handle both the definite-length case (decrementing length by
bytes.length) and the indefinite-length case (loop until EOC), referencing the
readSequence, readLength, readNext and readTag methods to locate and change the
logic.

In
@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java:
- Around line 99-165: Validate inputs and remove dead code: in
concatenatedRSToASN1DER check that signature != null, signLength > 0, signLength
is even, and signature.length == signLength before any System.arraycopy; remove
the unused ASN1Encoder.create().write(...) calls and only build/write the DER
sequence once. In asn1derToConcatenatedRS validate the sequence size==2 (already
present) and ensure the decoded BigInteger byte lengths are not larger than len
in a way that would cause silent truncation — if a DER integer yields more than
len bytes and is not a single leading zero padding byte, throw an IOException.
Harden integerToBytes to validate qLength > 0 and when bytes.length > qLength
allow trimming only if bytes.length == qLength+1 and bytes[0] == 0 (strip the
leading zero), otherwise throw an IOException; keep the existing left-padding
behavior when bytes.length < qLength.
- Around line 60-72: getBouncyCastleProvider() currently returns the JDK
KeyStore provider (e.g., "SUN") instead of a BouncyCastle provider; update
AuthzClientCryptoProvider.getBouncyCastleProvider() so it returns a genuine
BouncyCastle Provider instance (e.g., obtain Security.getProvider("BC") and if
null instantiate org.bouncycastle.jce.provider.BouncyCastleProvider and register
it via Security.addProvider(...) before returning it) so callers like
BouncyIntegration.loadProvider() receive the correct provider; keep the existing
order() value.

In
@authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java:
- Around line 41-43: The test currently creates a generic EC key pair in the
ECDSAAlgorithmTest() constructor (keyPair =
KeyPairGenerator.getInstance("EC").genKeyPair()) which doesn’t guarantee the
curve required by ES256/ES384/ES512; update the tests to generate curve-specific
key pairs instead: create separate key pairs using
KeyPairGenerator.getInstance("EC") initialized with
ECGenParameterSpec("secp256r1"), ECGenParameterSpec("secp384r1"), and
ECGenParameterSpec("secp521r1") respectively (e.g., in per-test setup methods or
by parameterizing the tests) so each ECDSA algorithm uses the correct curve for
ES256, ES384, and ES512 while retaining the existing keyPair variable name or
providing distinct names tied to the algorithm under test.
🧹 Nitpick comments (4)
common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java (1)

64-70: Consider cleaning up the trailing comma in the log message.

The current implementation appends ", " after each provider name, resulting in a trailing comma and space in the log output (e.g., "Ignored crypto providers: Provider1, Provider2, ").

♻️ Suggested cleanup for log message
 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());
+    String ignoredProviders = foundProviders.stream()
+            .skip(1)
+            .map(p -> p.getClass().getName())
+            .collect(Collectors.joining(", "));
+    logger.debugf("Ignored crypto providers: %s", ignoredProviders);
 }
authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java (1)

46-57: Consider adding signature verification to strengthen the test.

The current test validates that the conversion round-trip (DER -> RS -> DER -> RS) produces identical results, which tests the conversion logic but doesn't verify that the signatures themselves remain valid after conversion. Adding signature verification would provide stronger assurance that the conversions preserve cryptographic correctness.

✅ Proposed enhancement to add signature verification
 private void test(ECDSAAlgorithm algorithm, String curveName) throws Exception {
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
+    keyGen.initialize(new java.security.spec.ECGenParameterSpec(curveName));
+    KeyPair keyPair = keyGen.genKeyPair();
+    
     AuthzClientCryptoProvider prov = new AuthzClientCryptoProvider();
     byte[] data = "Something to sign".getBytes(StandardCharsets.UTF_8);
     Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm.name()));
     signature.initSign(keyPair.getPrivate());
     signature.update(data);
     byte[] sign = signature.sign();
+    
+    // Verify original signature
+    Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm.name()));
+    verifier.initVerify(keyPair.getPublic());
+    verifier.update(data);
+    Assert.assertTrue("Original signature should be valid", verifier.verify(sign));
+    
     byte[] rsConcat = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(sign, algorithm.getSignatureLength());
     byte[] asn1Des = prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat, algorithm.getSignatureLength());
+    
+    // Verify reconstructed DER signature
+    verifier.initVerify(keyPair.getPublic());
+    verifier.update(data);
+    Assert.assertTrue("Reconstructed DER signature should be valid", verifier.verify(asn1Des));
+    
     byte[] rsConcat2 = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(asn1Des, algorithm.getSignatureLength());
     Assert.assertArrayEquals(rsConcat, rsConcat2);
 }
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java (1)

30-99: Tighten API/clarity a bit (package-private class doesn’t need public members; length-byte write could be clearer).

Since ASN1Encoder is package-private, public on its methods doesn’t expand access but does add noise; consider dropping public keywords. Also consider masking in writeLength ((length >>> i) & 0xFF) for readability.

Proposed tweak
-    static public ASN1Encoder create() {
+    static ASN1Encoder create() {
         return new ASN1Encoder();
     }

-    public ASN1Encoder write(BigInteger value) throws IOException {
+    ASN1Encoder write(BigInteger value) throws IOException {
         writeEncoded(INTEGER, value.toByteArray());
         return this;
     }

-    public ASN1Encoder writeDerSeq(ASN1Encoder... objects) throws IOException {
+    ASN1Encoder writeDerSeq(ASN1Encoder... objects) throws IOException {
         writeEncoded(CONSTRUCTED | SEQUENCE, concatenate(objects));
         return this;
     }

-            for (int i = (size - 1) * 8; i >= 0; i -= 8) {
-                write((byte) (length >> i));
+            for (int i = (size - 1) * 8; i >= 0; i -= 8) {
+                write((byte) ((length >>> i) & 0xFF));
             }
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java (1)

74-225: Consider improving error messages for unsupported methods or add defensive test coverage.

The unsupported methods (getCertificateUtils, getPemUtils, getSignature, etc.) are not called in authz-client code paths—only getEcdsaCryptoProvider() and getKeyStore() are used. However, if future code paths or dependencies inadvertently invoke these, the generic exception message will be unhelpful for debugging. Consider either:

  • Adding context-specific exception messages (e.g., "ECDSA signature conversions only; full crypto suite not available in authz-client")
  • Adding a minimal integration test asserting the supported surface explicitly
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10aca55 and b95d12a.

📒 Files selected for processing (12)
  • authz/client/pom.xml
  • authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
  • 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
  • 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
  • authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java
  • common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java
  • common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java
  • 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
🧰 Additional context used
🧬 Code graph analysis (3)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java (1)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java (1)
  • ASN1Encoder (30-100)
crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java (2)
js/apps/admin-ui/src/authentication/execution-model.ts (1)
  • order (72-81)
core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java (1)
  • JavaAlgorithm (19-148)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java (2)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java (1)
  • ASN1Encoder (30-100)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java (1)
  • ASN1Decoder (33-202)
🔇 Additional comments (10)
authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider (1)

20-20: LGTM! Service provider registration is correctly formatted.

The registration of AuthzClientCryptoProvider follows the standard Java SPI pattern and will enable runtime discovery via ServiceLoader.

crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java (1)

82-85: LGTM! Provider ordering correctly implemented.

The order() method returns 200, establishing DefaultCryptoProvider as a high-priority provider in the selection mechanism.

crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java (1)

113-116: Verify the ordering strategy when multiple providers share the same priority.

The order() method returns 200, which is the same value as DefaultCryptoProvider and the mentioned WildFlyElytronProvider. When multiple providers with identical order values are present on the classpath, the selection becomes dependent on ServiceLoader iteration order, which is not guaranteed to be deterministic.

This may be intentional if any of these providers is considered acceptable when multiple are available. However, if there's a preference between FIPS1402Provider and DefaultCryptoProvider, consider using distinct order values.

common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java (1)

56-58: LGTM! Provider sorting correctly prioritizes higher order values.

The descending sort ensures that providers with higher order() values are evaluated first, implementing the documented precedence correctly.

common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java (1)

39-44: All CryptoProvider implementations have been properly updated with the order() method. Verification confirms:

  • DefaultCryptoProvider: order() returns 200
  • FIPS1402Provider: order() returns 200
  • WildFlyElytronProvider: order() returns 200
  • AuthzClientCryptoProvider: order() returns 100
authz/client/pom.xml (1)

62-71: LGTM!

Test dependencies are correctly scoped and will inherit versions from the parent POM, which is the standard approach for Maven multi-module projects.

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

127-167: Approve length validation with minor observation.

The readLength() method correctly:

  • Handles short-form (< 128), long-form, and indefinite-length encoding
  • Validates length is non-negative after reconstruction
  • Checks length doesn't exceed the limit
  • Restricts long-form to 4 bytes maximum

Note: The indefinite-length return value (-1) requires proper handling by callers, which should be verified.


169-182: LGTM!

The read(int length) method correctly handles partial reads from the underlying stream and provides clear error messages when EOF is encountered prematurely.

authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java (1)

95-95: CryptoIntegration.init() is idempotent and safe to call multiple times.

The method includes an explicit guard clause (if (isInitialised()) return;) that checks if the provider has already been initialized. Subsequent calls after the first initialization return immediately without repeating provider discovery, preventing repeated initialization overhead or potential resource issues.

crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java (1)

75-78: Consider documenting the provider ordering strategy and mutual exclusivity of crypto implementations.

All three main crypto providers (DefaultCryptoProvider, FIPS1402Provider, WildFlyElytronProvider) return order() = 200, giving them equal precedence. While the code selects the first provider after sorting by order (reversed), when multiple providers have identical order values, the selection depends on ServiceLoader discovery order, which can vary based on classpath order.

This is likely intentional since these providers are designed as mutually exclusive alternatives for different deployment scenarios (default, FIPS-compliant, and WildFly-based crypto implementations). However, if multiple crypto provider modules are accidentally included on the classpath, the selection would be non-deterministic. Consider documenting this design decision and the mutual exclusivity expectation, or adding a warning if multiple providers with equal order are detected at runtime (beyond the current debug logging).

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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle indefinite-length SEQUENCE encoding.

The readSequence() method only handles definite-length encoding. The readLength() method can return -1 for indefinite-length encoding (when encountering 0x80), but the while (length > 0) loop at line 57 will not execute for indefinite-length sequences, resulting in an empty list being returned incorrectly.

DER encoding typically uses definite length, but if this decoder is intended to handle BER (which allows indefinite length), this is a correctness issue.

🔧 Proposed fix to handle indefinite length or reject it explicitly

Option 1: Reject indefinite-length encoding explicitly (if only DER is supported):

 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();
+    if (length == -1) {
+        throw new IOException("Indefinite-length SEQUENCE encoding is not supported");
+    }
     List<byte[]> result = new ArrayList<>();
     while (length > 0) {
         byte[] bytes = readNext();
         result.add(bytes);
         length = length - bytes.length;
     }
     return result;
 }

Option 2: Support indefinite-length encoding (if BER support is needed):

 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<>();
+    if (length == -1) {
+        // Indefinite-length: read until end-of-contents octets (0x00 0x00)
+        while (true) {
+            mark();
+            int nextTag = readTag();
+            reset();
+            if (nextTag == 0x00) {
+                read(); // consume first 0x00
+                if (read() != 0x00) {
+                    throw new IOException("Invalid end-of-contents octets");
+                }
+                break;
+            }
+            result.add(readNext());
+        }
+    } else {
-    while (length > 0) {
-        byte[] bytes = readNext();
-        result.add(bytes);
-        length = length - bytes.length;
-    }
+        while (length > 0) {
+            byte[] bytes = readNext();
+            result.add(bytes);
+            length = length - bytes.length;
+        }
+    }
     return result;
 }
📝 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
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;
}
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();
if (length == -1) {
throw new IOException("Indefinite-length SEQUENCE encoding is not supported");
}
List<byte[]> result = new ArrayList<>();
while (length > 0) {
byte[] bytes = readNext();
result.add(bytes);
length = length - bytes.length;
}
return result;
}
🤖 Prompt for AI Agents
In
@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java
around lines 49 - 63, readSequence currently assumes definite-length; call
readLength() and if it returns -1 (indefinite-length) then explicitly reject it
by throwing an IOException (e.g. "Indefinite-length SEQUENCE not supported") or,
if BER support is required, implement an indefinite-length branch that
repeatedly reads elements (using readNext()/readTag()/readLength() as needed)
until an EOC (tag 0 and length 0) is encountered; update the while loop in
readSequence to handle both the definite-length case (decrementing length by
bytes.length) and the indefinite-length case (loop until EOC), referencing the
readSequence, readLength, readNext and readTag methods to locate and change the
logic.

Comment on lines +93 to +125
int readTagNumber(int tag) throws IOException {
int tagNo = tag & 0x1f;

//
// with tagged object tag number is bottom 5 bits, or stored at the start of the content
//
if (tagNo == 0x1f) {
tagNo = 0;

int b = read();

// X.690-0207 8.1.2.4.2
// "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
if ((b & 0x7f) == 0) // Note: -1 will pass
{
throw new IOException("corrupted stream - invalid high tag number found");
}

while ((b >= 0) && ((b & 0x80) != 0)) {
tagNo |= (b & 0x7f);
tagNo <<= 7;
b = read();
}

if (b < 0) {
throw new EOFException("EOF found inside tag value.");
}

tagNo |= (b & 0x7f);
}

return tagNo;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential integer overflow in high tag number calculation.

At lines 112-113, the tag number is constructed by repeatedly left-shifting and ORing values:

tagNo |= (b & 0x7f);
tagNo <<= 7;

For extremely large tag numbers (which are theoretically possible in ASN.1), this could overflow the int type after approximately 4-5 octets. While such large tag numbers are rare in practice, this could be exploited with malicious input.

🛡️ Proposed fix to add overflow detection
 int readTagNumber(int tag) throws IOException {
     int tagNo = tag & 0x1f;
 
     //
     // with tagged object tag number is bottom 5 bits, or stored at the start of the content
     //
     if (tagNo == 0x1f) {
         tagNo = 0;
 
         int b = read();
 
         // X.690-0207 8.1.2.4.2
         // "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
         if ((b & 0x7f) == 0) // Note: -1 will pass
         {
             throw new IOException("corrupted stream - invalid high tag number found");
         }
 
         while ((b >= 0) && ((b & 0x80) != 0)) {
+            if (tagNo > (Integer.MAX_VALUE >>> 7)) {
+                throw new IOException("Tag number overflow");
+            }
             tagNo |= (b & 0x7f);
             tagNo <<= 7;
             b = read();
         }
 
         if (b < 0) {
             throw new EOFException("EOF found inside tag value.");
         }
 
         tagNo |= (b & 0x7f);
     }
 
     return tagNo;
 }
🤖 Prompt for AI Agents
In
@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java
around lines 93 - 125, In readTagNumber(int tag) the loop builds tagNo by ORing
and then left-shifting which can overflow an int for maliciously large high-tag
encodings; fix by adding overflow detection before each left shift (or use a
wider accumulator) and throw an IOException if the next shift/OR would exceed
int capacity. Concretely, in readTagNumber replace the blind "tagNo |= (b &
0x7f); tagNo <<= 7;" sequence with logic that either accumulates into a long and
validates it fits into int before assigning back to tagNo, or checks whether
(tagNo >>> (32 - 7)) != 0 (or equivalent) and throws "corrupted stream - tag
number too large" to prevent integer wraparound while parsing.

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

@Override
public int order() {
return 100;
}

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

# 1) Find CryptoProvider interface definition
echo "=== CryptoProvider interface definition ==="
find . -name "*.java" -type f | xargs grep -l "interface.*CryptoProvider\|class.*CryptoProvider" | head -5

# 2) Search for CryptoProvider interface
rg -n "interface\s+CryptoProvider" -A 20

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 598


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where getBouncyCastleProvider is called
echo "=== Calls to getBouncyCastleProvider() ==="
rg -n "getBouncyCastleProvider\s*\(" -C 3

# Find where CryptoProvider instances are used
echo "=== CryptoProvider usage ==="
rg -n "CryptoProvider" --max-count=20

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 19605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for ServiceLoader usage with CryptoProvider
echo "=== ServiceLoader + CryptoProvider ==="
rg -n "ServiceLoader.*CryptoProvider\|CryptoProvider.*load" -C 2

# Search for order() method calls
echo "=== order() method implementations ==="
rg -n "\.order\(\)" -C 2

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check AuthzClientCryptoProvider's order() method
echo "=== AuthzClientCryptoProvider.order() ==="
rg -n "class AuthzClientCryptoProvider" -A 50 authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java | grep -A 3 "order()"

# Check other providers' order() for comparison
echo -e "\n=== Order values for other CryptoProviders ==="
rg -n "order\(\)" -B 2 -A 1 crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java
rg -n "order\(\)" -B 2 -A 1 crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java
rg -n "order\(\)" -B 2 -A 1 crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 457


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full BouncyIntegration context
echo "=== BouncyIntegration context ==="
cat -n common/src/main/java/org/keycloak/common/util/BouncyIntegration.java | head -50

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 1843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if authz-client is lightweight and doesn't use full CryptoIntegration
echo "=== AuthzClientCryptoProvider usage context ==="
grep -r "CryptoIntegration\|AuthzClientCryptoProvider" authz/client/src/ | grep -v ".class" | head -20

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 760


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the full AuthzClientCryptoProvider file to understand its design/purpose
echo "=== Full AuthzClientCryptoProvider context ==="
cat -n authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java | head -100

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 4185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what methods actually get called on the selected CryptoProvider in authz-client
echo "=== Methods called from CryptoIntegration.getProvider() in authz-client ==="
grep -n "CryptoIntegration\|getProvider()" authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java -A 3 -B 3

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 837


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look at CryptoIntegration.detectProvider to see how it selects the provider when multiple exist
echo "=== CryptoIntegration provider selection logic ==="
cat -n common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java | sed -n '55,70p'

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 1249


getBouncyCastleProvider() returns the wrong provider type and violates the documented contract.

The method returns KeyStore.getInstance(KeyStore.getDefaultType()).getProvider(), which returns the JDK keystore provider (typically "SUN"), not BouncyCastle. The CryptoProvider interface contract explicitly documents this method should return "BouncyCastle security provider. Can be either non-FIPS or FIPS based provider."

While this provider has order()=100 (lower than other implementations at 200), meaning it won't be selected in typical deployments where real crypto providers are available, it should still be fixed. If AuthzClientCryptoProvider is the only provider on the classpath (e.g., in a thin client scenario), callers like BouncyIntegration.loadProvider() will receive the wrong provider name, potentially causing cryptographic operations to use incorrect providers.

🤖 Prompt for AI Agents
In
@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java
around lines 60 - 72, getBouncyCastleProvider() currently returns the JDK
KeyStore provider (e.g., "SUN") instead of a BouncyCastle provider; update
AuthzClientCryptoProvider.getBouncyCastleProvider() so it returns a genuine
BouncyCastle Provider instance (e.g., obtain Security.getProvider("BC") and if
null instantiate org.bouncycastle.jce.provider.BouncyCastleProvider and register
it via Security.addProvider(...) before returning it) so callers like
BouncyIntegration.loadProvider() receive the correct provider; keep the existing
order() value.

Comment on lines +99 to +165
@Override
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;
}
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ECDSA RS<->DER conversion needs input validation + remove dead code + avoid truncation.

  • concatenatedRSToASN1DER: no check that signature.length == signLength (can crash with System.arraycopy), and signLength odd/invalid isn’t rejected. Also Lines 114-115 are unused.
  • asn1derToConcatenatedRS/integerToBytes: oversized/negative DER integers can get silently truncated to len bytes, producing incorrect signatures without an explicit error.
Proposed hardening (minimal, keeps current structure)
@@
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
 import java.security.spec.ECParameterSpec;
 import java.util.List;
+import java.util.Arrays;
 import javax.crypto.Cipher;
@@
             @Override
             public byte[] concatenatedRSToASN1DER(byte[] signature, int signLength) throws IOException {
+                if (signature == null) {
+                    throw new IOException("Signature must not be null");
+                }
+                if (signLength <= 0 || (signLength & 1) != 0) {
+                    throw new IOException("Invalid signLength: " + signLength);
+                }
+                if (signature.length != signLength) {
+                    throw new IOException("Invalid signature length " + signature.length + ", expected " + signLength);
+                }
                 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);
+                BigInteger rBigInteger = new BigInteger(1, Arrays.copyOfRange(signature, 0, len));
+                BigInteger sBigInteger = new BigInteger(1, Arrays.copyOfRange(signature, len, signLength));
@@
             public byte[] asn1derToConcatenatedRS(byte[] derEncodedSignatureValue, int signLength) throws IOException {
+                if (derEncodedSignatureValue == null) {
+                    throw new IOException("DER signature must not be null");
+                }
+                if (signLength <= 0 || (signLength & 1) != 0) {
+                    throw new IOException("Invalid signLength: " + signLength);
+                }
                 int len = signLength / 2;
@@
                 BigInteger rBigInteger = ASN1Decoder.create(seq.get(0)).readInteger();
                 BigInteger sBigInteger = ASN1Decoder.create(seq.get(1)).readInteger();
+                if (rBigInteger.signum() < 0 || sBigInteger.signum() < 0) {
+                    throw new IOException("Invalid DER signature: negative integer");
+                }
@@
             private byte[] integerToBytes(BigInteger s, int qLength) {
                 byte[] bytes = s.toByteArray();
+                // Accept one leading 0x00 sign byte, but reject true overflow beyond qLength.
+                if (bytes.length > qLength + 1 || (bytes.length == qLength + 1 && bytes[0] != 0)) {
+                    throw new IllegalArgumentException("Integer does not fit expected length " + qLength);
+                }
                 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;
             }
🤖 Prompt for AI Agents
In
@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java
around lines 99 - 165, Validate inputs and remove dead code: in
concatenatedRSToASN1DER check that signature != null, signLength > 0, signLength
is even, and signature.length == signLength before any System.arraycopy; remove
the unused ASN1Encoder.create().write(...) calls and only build/write the DER
sequence once. In asn1derToConcatenatedRS validate the sequence size==2 (already
present) and ensure the decoded BigInteger byte lengths are not larger than len
in a way that would cause silent truncation — if a DER integer yields more than
len bytes and is not a single leading zero padding byte, throw an IOException.
Harden integerToBytes to validate qLength > 0 and when bytes.length > qLength
allow trimming only if bytes.length == qLength+1 and bytes[0] == 0 (strip the
leading zero), otherwise throw an IOException; keep the existing left-padding
behavior when bytes.length < qLength.

Comment on lines +41 to +43
public ECDSAAlgorithmTest() throws Exception {
keyPair = KeyPairGenerator.getInstance("EC").genKeyPair();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Specify EC curve to match ECDSA algorithm requirements.

The constructor generates a generic EC key pair without specifying the curve. Each ECDSA algorithm requires a specific curve:

  • ES256 requires P-256 (secp256r1)
  • ES384 requires P-384 (secp384r1)
  • ES512 requires P-521 (secp521r1)

Using a single key pair with an unspecified curve for all three tests may cause failures depending on the JVM's default curve, and doesn't properly test the algorithm-specific behavior.

🔧 Proposed fix to generate curve-specific key pairs
-public class ECDSAAlgorithmTest {
-
-    private final KeyPair keyPair;
-
-    public ECDSAAlgorithmTest() throws Exception {
-        keyPair = KeyPairGenerator.getInstance("EC").genKeyPair();
-    }
-
-
-    private void test(ECDSAAlgorithm algorithm) throws Exception {
+public class ECDSAAlgorithmTest {
+
+    private void test(ECDSAAlgorithm algorithm, String curveName) throws Exception {
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
+        keyGen.initialize(new java.security.spec.ECGenParameterSpec(curveName));
+        KeyPair keyPair = keyGen.genKeyPair();
+        
         AuthzClientCryptoProvider prov = new AuthzClientCryptoProvider();
         byte[] data = "Something to sign".getBytes(StandardCharsets.UTF_8);
         Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm.name()));
         signature.initSign(keyPair.getPrivate());
         signature.update(data);
         byte[] sign = signature.sign();
         byte[] rsConcat = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(sign, algorithm.getSignatureLength());
         byte[] asn1Des = prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat, algorithm.getSignatureLength());
         byte[] rsConcat2 = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(asn1Des, algorithm.getSignatureLength());
         Assert.assertArrayEquals(rsConcat, rsConcat2);
     }
 
     @Test
     public void testES256() throws Exception {
-        test(ECDSAAlgorithm.ES256);
+        test(ECDSAAlgorithm.ES256, "secp256r1");
     }
 
     @Test
     public void testES384() throws Exception {
-        test(ECDSAAlgorithm.ES384);
+        test(ECDSAAlgorithm.ES384, "secp384r1");
     }
 
     @Test
     public void testES512() throws Exception {
-        test(ECDSAAlgorithm.ES512);
+        test(ECDSAAlgorithm.ES512, "secp521r1");
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java
around lines 41 - 43, The test currently creates a generic EC key pair in the
ECDSAAlgorithmTest() constructor (keyPair =
KeyPairGenerator.getInstance("EC").genKeyPair()) which doesn’t guarantee the
curve required by ES256/ES384/ES512; update the tests to generate curve-specific
key pairs instead: create separate key pairs using
KeyPairGenerator.getInstance("EC") initialized with
ECGenParameterSpec("secp256r1"), ECGenParameterSpec("secp384r1"), and
ECGenParameterSpec("secp521r1") respectively (e.g., in per-test setup methods or
by parameterizing the tests) so each ECDSA algorithm uses the correct curve for
ES256, ES384, and ES512 while retaining the existing keyPair variable name or
providing distinct names tied to the algorithm under test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants