Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

package com.ongres.scram.client;

import java.util.Locale;

import org.jetbrains.annotations.NotNull;

/**
* Defines the client-side security policies for negotiating channel binding during a SCRAM
* authentication execution.
Expand Down Expand Up @@ -46,9 +50,10 @@ public enum ChannelBindingPolicy {
* <li>If the server advertises a {@code -PLUS} mechanism and channel binding data is provided,
* the negotiation upgrades to channel binding and uses the {@code 'p'} GS2 flag.</li>
* <li>If the server lacks {@code -PLUS} support, the client gracefully downgrades to standard
* SCRAM but transmits the {@code 'y'} GS2 flag. This explicitly declares to the server
* that the client possesses channel binding capabilities, allowing the server to catch
* and terminate malicious downgrade attacks mid-flight.</li>
* SCRAM. If channel binding data was provided, the {@code 'y'} GS2 flag is transmitted to
* explicitly declare that the client possesses channel binding capabilities, allowing the server
* to catch and terminate malicious downgrade attacks mid-flight. If no binding data was
* configured, {@code 'n'} is used instead.</li>
* </ul>
*/
ALLOW,
Expand All @@ -70,5 +75,37 @@ public enum ChannelBindingPolicy {
* applications requiring strict compliance matching database configurations like PostgreSQL's
* {@code channel_binding=require}).
*/
REQUIRE
REQUIRE;

/**
* Returns the {@link ChannelBindingPolicy} constant for the given string value
* (case-insensitive).
*
* <p>Accepted values:
* <ul>
* <li>{@code "disable"} → {@link #DISABLE}</li>
* <li>{@code "allow"} → {@link #ALLOW}</li>
* <li>{@code "prefer"} → {@link #ALLOW} (alias for PostgreSQL {@code channel_binding=prefer}
* compatibility)</li>
* <li>{@code "require"} → {@link #REQUIRE}</li>
* </ul>
*
* @param value the string representation of the policy
* @return the matching {@link ChannelBindingPolicy}
* @throws IllegalArgumentException if {@code value} does not match any known policy
*/
public static ChannelBindingPolicy of(@NotNull String value) {
String lowerValue = value.toLowerCase(Locale.ROOT);
switch (lowerValue) {
case "disable":
return DISABLE;
case "allow":
case "prefer":
return ALLOW;
case "require":
return REQUIRE;
default:
throw new IllegalArgumentException("Invalid channel binding value: " + value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2026 OnGres, Inc.
* SPDX-License-Identifier: BSD-2-Clause
*/

package com.ongres.scram.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class ChannelBindingPolicyTest {

@ParameterizedTest(name = "''{0}'' -> DISABLE")
@ValueSource(strings = {"disable", "DISABLE", "Disable"})
void ofDisable(String value) {
assertEquals(ChannelBindingPolicy.DISABLE, ChannelBindingPolicy.of(value));
}

@ParameterizedTest(name = "''{0}'' -> ALLOW")
@ValueSource(strings = {"allow", "ALLOW", "Allow", "prefer", "PREFER", "Prefer"})
void ofAllow(String value) {
assertEquals(ChannelBindingPolicy.ALLOW, ChannelBindingPolicy.of(value));
}

@ParameterizedTest(name = "''{0}'' -> REQUIRE")
@ValueSource(strings = {"require", "REQUIRE", "Require"})
void ofRequire(String value) {
assertEquals(ChannelBindingPolicy.REQUIRE, ChannelBindingPolicy.of(value));
}

@Test
void ofUnknownValueThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> ChannelBindingPolicy.of("unknown"));
assertEquals("Invalid channel binding value: unknown", ex.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
import java.security.cert.X509Certificate;
import java.security.spec.PSSParameterSpec;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.jetbrains.annotations.NotNull;

Expand All @@ -38,10 +37,14 @@ public final class TlsServerEndpoint {
* A static mapping of universally standard signature OIDs to their underlying digest algorithms.
* This guarantees resolution even if the JCE provider fails to provide a friendly string name.
*/
private static final Map<String, String> OID_TO_DIGEST;
private static final Map<String, String> OID_TO_DIGEST = digestAlgorithmByOid();

static {
ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
private TlsServerEndpoint() {
throw new IllegalStateException("Utility class");
}

private static Map<String, String> digestAlgorithmByOid() {
Map<String, String> map = new HashMap<>(64);
// OIDs associated with RSA Signatures
map.put("1.2.840.113549.1.1.4", "MD5"); // MD5withRSA
map.put("1.2.840.113549.1.1.5", "SHA-1"); // SHA1withRSA
Expand Down Expand Up @@ -75,12 +78,7 @@ public final class TlsServerEndpoint {
map.put("2.16.840.1.101.3.4.3.6", "SHA3-256"); // SHA3-256withDSA
map.put("2.16.840.1.101.3.4.3.7", "SHA3-384"); // SHA3-384withDSA
map.put("2.16.840.1.101.3.4.3.8", "SHA3-512"); // SHA3-512withDSA

OID_TO_DIGEST = Collections.unmodifiableMap(map);
}

private TlsServerEndpoint() {
throw new IllegalStateException("Utility class");
return Collections.unmodifiableMap(map);
}

/**
Expand Down Expand Up @@ -146,16 +144,18 @@ private static MessageDigest getDigestAlgorithm(final X509Certificate serverCert

// Normalize the hash name (Applies to both Traditional and RSASSA-PSS)
switch (algorithmName) {
// Enforce RFC 5929 Mandatory Upgrades to SHA-256
case "MD5":
case "SHA1":
case "SHA-1":
case "SHA256":
// Enforce RFC 5929 Mandatory Upgrades to SHA-256
algorithmName = "SHA-256";
break;
case "SHA224":
algorithmName = "SHA-224";
break;
case "SHA256":
algorithmName = "SHA-256";
break;
case "SHA384":
algorithmName = "SHA-384";
break;
Expand All @@ -177,28 +177,6 @@ private static MessageDigest getDigestAlgorithm(final X509Certificate serverCert
return MessageDigest.getInstance(algorithmName);
}

/**
* The hash of the TLS server's certificate [RFC5280] as it appears, octet for octet, in the
* server's Certificate message.
*
* @param serverCert the TLS server's peer certificate
* @return the hash of the TLS server's peer certificate
* @throws CertificateEncodingException if an encoding error occurs.
* @deprecated this method silently swallows {@link NoSuchAlgorithmException} and returns an
* empty array. It is replaced by {@link #getChannelBindingHash(X509Certificate)}
* and will be removed in a future release.
*/
@Deprecated
public static byte @NotNull [] getChannelBindingData(final @NotNull X509Certificate serverCert)
throws CertificateEncodingException {
try {
return getChannelBindingHash(serverCert);
} catch (NoSuchAlgorithmException e) {
// Preserve the old (and dangerous) silent-failure behavior for backward compatibility
return new byte[0];
}
}

/**
* Computes the hash of the TLS server's certificate [RFC5280] as it appears, octet for octet, in
* the server's Certificate message, for use as {@code "tls-server-end-point"} channel binding.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ private static Stream<Arguments> provideValidCertificates() {
Arguments.of(loadCertificate("/SHA3-512withECDSA.pem"),
"SHA3-512withECDSA", "SHA3-512"),

// Parameterized RSASSA-PSS (The generated PEM uses SHA-384)
// Parameterized RSASSA-PSS with SHA-384 digest
Arguments.of(loadCertificate("/RSASSA-PSS.pem"),
"RSASSA-PSS", "SHA-384"));
"RSASSA-PSS", "SHA-384"),

// RSASSA-PSS with SHA-1 digest: RFC 5929 mandatory upgrade SHA-1 -> SHA-256
Arguments.of(loadCertificate("/RSASSA-PSS-SHA1.pem"),
"RSASSA-PSS", "SHA-256"));
}

/**
Expand All @@ -68,7 +72,6 @@ private static Stream<Arguments> provideUnsupportedCertificates() {
Arguments.of(loadCertificate("/Ed448.pem"), "Ed448"));
}

@SuppressWarnings("deprecation")
@ParameterizedTest(name = "Extracts correct binding hash for {1} -> expects {2}")
@MethodSource("provideValidCertificates")
void testValidCertificateChannelBinding(X509Certificate cert, String sigAlg,
Expand All @@ -87,34 +90,23 @@ void testValidCertificateChannelBinding(X509Certificate cert, String sigAlg,
byte[] actualHash = assertDoesNotThrow(() -> TlsServerEndpoint.getChannelBindingHash(cert));
assertArrayEquals(expectedHash, actualHash,
"getChannelBindingHash did not return the expected digest bytes");

// 4. Test the deprecated method
byte[] actualDeprecatedData = assertDoesNotThrow(() -> TlsServerEndpoint.getChannelBindingData(cert));
assertArrayEquals(expectedHash, actualDeprecatedData,
"Deprecated getChannelBindingData did not match expected digest bytes");
}

@SuppressWarnings({ "deprecation" })
@ParameterizedTest(name = "Rejects unsupported/pure algorithm {1} (Postgres parity)")
@MethodSource("provideUnsupportedCertificates")
void testUnsupportedCertificatesFail(X509Certificate cert, String sigAlg) {
// Ensure the generated PEM matches the test expectation
assertEquals(sigAlg, cert.getSigAlgName(),
"Loaded certificate does not match expected algorithm");

// 1. Ensure the modern method throws an exception
// Ensure the modern method throws an exception
NoSuchAlgorithmException exception = assertThrows(NoSuchAlgorithmException.class,
() -> TlsServerEndpoint.getChannelBindingHash(cert));

assertEquals(String.format(Locale.ROOT,
"Could not determine server certificate signature algorithm. Name: %s, OID: %s",
cert.getSigAlgName(), cert.getSigAlgOID()),
exception.getMessage());

// 2. Ensure the deprecated method swallows the exception and returns an empty array
byte[] deprecatedResult = assertDoesNotThrow(() -> TlsServerEndpoint.getChannelBindingData(cert));
assertArrayEquals(new byte[0], deprecatedResult,
"Deprecated method must return byte[0] on unsupported algorithms");
}

/**
Expand Down
19 changes: 19 additions & 0 deletions scram-common/src/test/resources/RSASSA-PSS-SHA1.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDHzCCAgegAwIBAgIUSkhWn0yS4ZIwnUvJo1mIhcwvCfowDQYJKoZIhvcNAQEK
MAAwHzEdMBsGA1UEAwwUVGVzdCBSU0FTU0EtUFNTIFNIQTEwHhcNMjYwNjE1MjM0
NzUwWhcNMzYwNjEyMjM0NzUwWjAfMR0wGwYDVQQDDBRUZXN0IFJTQVNTQS1QU1Mg
U0hBMTCCASIwDQYJKoZIhvcNAQEKMAADggEPADCCAQoCggEBAMOoktXogxO3xRfc
UeD9rjadC6dGcTpJSZ4t+iLB1GvwsjJ67mTzaXnkJmzt/h52ZPP3VYkw2xpgVJUp
IEgMNKlEuYmxTIBQPY/lNVwu8o7nnm9eElU+4dr54uQ8he+5VJ5/HLxObBJ7qtjR
1YHzo43XMDydxE8Ls32q+6u5yXPJgWXY69VvhtbgnRKql5MtvFAd12DEaDWn1+fF
v8e1MN8k89EWWRWFOlOfC3tm9nLYAeRJx0bnX5JR7X+MkuijUUFg4T0NO4VzuZWq
Ev1Mumu0e+s7innTVV3FM/ll8PImcAVdvQtKAQReF7NGLQumFXKqS+bAU6/hvAGh
An4gZs0CAwEAAaNTMFEwHQYDVR0OBBYEFPhQ/8VeAhkYUV0WWgzW/bZSkSIbMB8G
A1UdIwQYMBaAFPhQ/8VeAhkYUV0WWgzW/bZSkSIbMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQEKMAADggEBAGRUZpWMnWsizY4iyGGL5zdXAUzuI3OfbBdheSm8
BSZdBWk8Ype8XyFs4mLVsUC9U8oMGMhKJTki1PVZCKiZvUhu9LxxhQF7qtUgOGKw
dOTIHlA1o4pPImleebCEuEMMpq1wn/AMNd40z9ife+Z4r9RXHGIxH5J8CcwmrzDI
dZLs1tjDyQO8LKQ0qhTY9p8UVEJDtYjl5JY1yqgv4KNEEB2c1v+VFZxsJmSiuhpI
PJbLla62vN1xefT6uwIXhxSyL+5/NRi9agdMjJUUsi+ZSO+Q8x7pY8gcnjppzOdA
C/doBBrosmzbQ+EYknkjUKj/NnR6BoYBD8qXnwDtZ4ntoVo=
-----END CERTIFICATE-----
Loading