8000 Upgrade WebAuthn4j to 0.32.0 with ML-DSA support by mabartos · Pull Request #54556 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
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
1 change: 1 addition & 0 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public enum Feature {
JWT_AUTHORIZATION_GRANT("JWT Profile for Oauth 2.0 Authorization Grant", Type.DEFAULT),

WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),
WEBAUTHN_ML_DSA("WebAuthn ML-DSA signature algorithms", Type.EXPERIMENTAL, Feature.WEB_AUTHN),

CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),

Expand Down
64 changes: 31 additions & 33 deletions core/src/main/java/org/keycloak/mdoc/CborUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/
package org.keycloak.mdoc;

import java.io.IOException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
Expand All @@ -25,42 +24,41 @@
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializable;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.JacksonSerializable;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.std.StdSerializer;
import tools.jackson.dataformat.cbor.CBORGenerator;
import tools.jackson.dataformat.cbor.CBORMapper;

final class CborUtil {

static final int TAG_TDATE = 0;
static final int TAG_ENCODED_CBOR = 24;

private static final ObjectConverter OBJECT_CONVERTER = createObjectConverter();
private static final CborConverter CBOR_CONVERTER = OBJECT_CONVERTER.getCborConverter();
private static final CBORMapper CBOR_MAPPER = OBJECT_CONVERTER.getCborMapper();
private static final DateTimeFormatter TDATE_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;

// ISO mdoc requires definite length CBOR encoding, while Jackson's default Map serializer emits indefinite
// length maps. Register a serializer that writes the map header with its size for every encoded map.
private static ObjectConverter createObjectConverter() {
SimpleModule definiteLengthMaps = new SimpleModule();
definiteLengthMaps.addSerializer(new DefiniteLengthMapSerializer());
ObjectMapper cborMapper = new ObjectMapper(new CBORFactory());
cborMapper.registerModule(definiteLengthMaps);
return new ObjectConverter(new ObjectMapper(), cborMapper);
CBORMapper cborMapper = CBORMapper.builder().addModule(definiteLengthMaps).build();
return new ObjectConverter(JsonMapper.builder().build(), cborMapper);
}

private CborUtil() {
}

static byte[] encode(Object value) {
return CBOR_CONVERTER.writeValueAsBytes(value);
return CBOR_MAPPER.writeValueAsBytes(value);
}

// COSE signatures cover the protected-header byte string exactly. Jackson's default Map serializer emits an
Expand All @@ -70,7 +68,7 @@ static byte[] encodeIntegerMap(Map<Integer, Integer> value) {
}

static Object decode(byte[] encoded) {
return CBOR_CONVERTER.readValue(encoded, Object.class);
return CBOR_MAPPER.readValue(encoded, Object.class);
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -142,7 +140,7 @@ static Tagged encodedCbor(Object value) {
return new Tagged(TAG_ENCODED_CBOR, encode(value));
}

static final class Tagged implements JsonSerializable {
static final class Tagged implements JacksonSerializable {

private final int tag;
private final Object value;
Expand All @@ -161,14 +159,14 @@ Object value() {
}

@Override
public void serialize(JsonGenerator generator, SerializerProvider provider) throws IOException {
public void serialize(JsonGenerator generator, SerializationContext provider) throws JacksonException {
((CBORGenerator) generator).writeTag(tag);
generator.writeObject(value);
generator.writePOJO(value);
}

@Override
public void serializeWithType(JsonGenerator generator, SerializerProvider provider, TypeSerializer typeSerializer)
throws IOException {
public void serializeWithType(JsonGenerator generator, SerializationContext provider, TypeSerializer typeSerializer)
throws JacksonException {
serialize(generator, provider);
}
}
Expand All @@ -180,23 +178,23 @@ static final class DefiniteLengthMapSerializer extends StdSerializer<Map<?, ?>>
}

@Override
public void serialize(Map<?, ?> value, JsonGenerator generator, SerializerProvider provider) throws IOException {
public void serialize(Map<?, ?> value, JsonGenerator generator, SerializationContext provider) throws JacksonException {
CBORGenerator cborGenerator = (CBORGenerator) generator;
cborGenerator.writeStartObject(value.size());
cborGenerator.writeStartObject(value, value.size());
for (Map.Entry<?, ?> entry : value.entrySet()) {
Object key = entry.getKey();
if (key instanceof Number) {
cborGenerator.writeFieldId(((Number) key).longValue());
cborGenerator.writePropertyId(((Number) key).longValue());
} else {
generator.writeFieldName(String.valueOf(key));
generator.writeName(String.valueOf(key));
}
generator.writeObject(entry.getValue());
generator.writePOJO(entry.getValue());
}
generator.writeEndObject();
}
}

static final class IntegerMap implements JsonSerializable {
static final class IntegerMap implements JacksonSerializable {

private final Map<Integer, Integer> value;

Expand All @@ -205,19 +203,19 @@ static final class IntegerMap implements JsonSerializable {
}

@Override
public void serialize(JsonGenerator generator, SerializerProvider provider) throws IOException {
public void serialize(JsonGenerator generator, SerializationContext provider) throws JacksonException {
CBORGenerator cborGenerator = (CBORGenerator) generator;
cborGenerator.writeStartObject(value.size());
cborGenerator.writeStartObject(value, value.size());
for (Map.Entry<Integer, Integer> entry : value.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList())) {
cborGenerator.writeFieldId(entry.getKey());
cborGenerator.writePropertyId(entry.getKey());
generator.writeNumber(entry.getValue());
}
generator.writeEndObject();
}

@Override
public void serializeWithType(JsonGenerator generator, SerializerProvider provider, TypeSerializer typeSerializer)
throws IOException {
public void serializeWithType(JsonGenerator generator, SerializationContext provider, TypeSerializer typeSerializer)
throws JacksonException {
serialize(generator, provider);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class CryptoInfoRepresentation {

private List<String> clientSignatureAsymmetricAlgorithms;

private List<String> webAuthnSignatureAlgorithms;

public String getCryptoProvider() {
return cryptoProvider;
}
Expand Down Expand Up @@ -64,4 +66,12 @@ public List<String> getClientSignatureAsymmetricAlgorithms() {
public void setClientSignatureAsymmetricAlgorithms(List<String> clientSignatureAsymmetricAlgorithms) {
this.clientSignatureAsymmetricAlgorithms = clientSignatureAsymmetricAlgorithms;
}

public List<String> getWebAuthnSignatureAlgorithms() {
return webAuthnSignatureAlgorithms;
}

public void setWebAuthnSignatureAlgorithms(List<String> webAuthnSignatureAlgorithms) {
this.webAuthnSignatureAlgorithms = webAuthnSignatureAlgorithms;
}
}
15 changes: 15 additions & 0 deletions core/src/test/java/org/keycloak/mdoc/CborUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.keycloak.mdoc;

import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
Expand Down Expand Up @@ -57,6 +58,20 @@ public void shouldEncodeMapsWithDefiniteLength() {
assertCborHex("a1 6161 01", CborUtil.encode(value));
}

@Test
public void shouldEncodeNestedMapsWithDefiniteLength() {
Map<String, Object> value = Collections.singletonMap("a", Collections.singletonMap("b", 1));

assertCborHex("a1 6161 a1 6162 01", CborUtil.encode(value));
}

@Test
public void shouldEncodeTaggedCborBytes() {
CborUtil.Tagged value = CborUtil.encodedCbor(Collections.singletonMap("a", 1));

assertCborHex("d818 44 a1 6161 01", CborUtil.encode(value));
}

@Test
public void shouldEncodeIntegerMapKeysAsCborIntegers() {
Map<Integer, Integer> value = new LinkedHashMap<>();
Expand Down
9 changes: 9 additions & 0 deletions docs/documentation/release_notes/topics/27_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ See the upgrading guide for migration details.

= Security and Standards

== Post-quantum WebAuthn signatures (experimental)

{project_name} introduces experimental support for `ML-DSA-44`, `ML-DSA-65`, and `ML-DSA-87` in WebAuthn policies.
Enable it with `--features=webauthn-ml-dsa`.
Using these algorithms requires a compatible browser, authenticator, and server cryptographic provider.
The default signature algorithms remain unchanged.

For requirements, see link:{adminguide_link}#_webauthn-pqc[Post-quantum signature algorithms] in the {adminguide_name}.

== PAR discovery respects the feature flag

When the `par` feature is disabled, OpenID Connect discovery no longer advertises the pushed authorization request endpoint, including its mTLS alias, or `require_pushed_authorization_requests`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The configurable items and their description are as follows:
|The readable server name as a WebAuthn Relying Party. This item is mandatory and applies to the registration of the WebAuthn authenticator. The default setting is "keycloak". For more details, see https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity[WebAuthn Specification].

|Signature Algorithms
|The algorithms telling the WebAuthn authenticator which signature algorithms to use for the https://www.w3.org/TR/webauthn/#iface-pkcredential[Public Key Credential]. {project_name} uses the Public Key Credential to sign and verify https://www.w3.org/TR/webauthn/#authentication-assertion[Authentication Assertions]. If no algorithms exist, the default https://datatracker.ietf.org/doc/html/rfc8152#section-8.1[ES256] and https://datatracker.ietf.org/doc/html/rfc7518#section-3.1[RS256] is adapted. ES256 and RS256 are an optional configuration item applying to the registration of WebAuthn authenticators. For more details, see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters[WebAuthn Specification].
|The algorithms telling the WebAuthn authenticator which signature algorithms to use for the https://www.w3.org/TR/webauthn/#iface-pkcredential[Public Key Credential]. {project_name} uses the Public Key Credential to sign and verify https://www.w3.org/TR/webauthn/#authentication-assertion[Authentication Assertions]. If no algorithms exist, the default https://datatracker.ietf.org/doc/html/rfc8152#section-8.1[ES256] and https://datatracker.ietf.org/doc/html/rfc7518#section-3.1[RS256] is adapted. ES256 and RS256 are an optional configuration item applying to the registration of WebAuthn authenticators. For ML-DSA requirements, see <<_webauthn-pqc,Post-quantum signature algorithms>>. For more details, see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters[WebAuthn Specification].

|Relying Party ID
|The ID of a WebAuthn Relying Party that determines the scope of https://www.w3.org/TR/webauthn/#public-key-credential[Public Key Credentials]. The ID must be the origin's effective domain. This ID is an optional configuration item applied to the registration of WebAuthn authenticators. If this entry is blank, {project_name} adapts the host part of {project_name}'s base URL. For more details, see https://www.w3.org/TR/webauthn/[WebAuthn Specification].
Expand Down Expand Up @@ -129,6 +129,20 @@ The configurable items and their description are as follows:

|===

[[_webauthn-pqc]]
==== Post-quantum signature algorithms

[NOTE]
====
ML-DSA support is experimental and available only when the `webauthn-ml-dsa` feature is enabled (`--features=webauthn-ml-dsa`).
====

WebAuthn policies support `ML-DSA-44`, `ML-DSA-65`, and `ML-DSA-87`, which require a compatible browser and authenticator.
The default algorithms remain `ES256` and `RS256`.

The server requires an ML-DSA-capable cryptographic provider, available in JDK 24 and later or through the bundled Bouncy Castle provider in non-FIPS mode.
In FIPS mode, availability depends on the configured provider and its approved algorithms.

==== Attestation statement verification

When registering a WebAuthn authenticator, {project_name} verifies the trustworthiness of the attestation statement generated by the WebAuthn authenticator. {project_name} requires the trust anchor's certificates imported into the https://www.keycloak.org/server/keycloak-truststore[truststore].
Expand Down
2 changes: 0 additions & 2 deletions integration/admin-client-jackson3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

<properties>
<maven.compiler.release>17</maven.compiler.release>
<version.tools.jackson>3.1.7</version.tools.jackson>
<version.resteasy.jackson.provider>1.0.0.Final</version.resteasy.jackson.provider>
</properties>

Expand All @@ -30,7 +29,6 @@
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${version.tools.jackson}</version>
</dependency>
<dependency>
<groupId>dev.resteasy.providers</groupId>
Expand Down
14 changes: 3 additions & 11 deletions js/apps/admin-ui/src/authentication/policies/WebauthnPolicy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,13 @@ import { FormAccess } from "../../components/form/FormAccess";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import { TimeSelectorControl } from "../../components/time-selector/TimeSelectorControl";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";

import { useAdminClient } from "../../admin-client";
import "./webauthn-policy.css";

const SIGNATURE_ALGORITHMS = [
"ES256",
"ES384",
"ES512",
"RS256",
"RS384",
"RS512",
"Ed25519",
"RS1",
] as const;
const ATTESTATION_PREFERENCE = [
"not specified",
"none",
Expand Down Expand Up @@ -129,6 +120,7 @@ export const WebauthnPolicy = ({
isPasswordLess = false,
}: WebauthnPolicyProps) => {
const { adminClient } = useAdminClient();
const { cryptoInfo } = useServerInfo();

const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
Expand Down Expand Up @@ -226,7 +218,7 @@ export const WebauthnPolicy = ({
name={`${namePrefix}SignatureAlgorithms`}
label={t("webAuthnPolicySignatureAlgorithms")}
labelIcon={t("webAuthnPolicySignatureAlgorithmsHelp")}
options={SIGNATURE_ALGORITHMS}
options={cryptoInfo?.webAuthnSignatureAlgorithms ?? []}
isMultiSelect
/>
<TextControl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ export interface CryptoInfoRepresentation {
supportedKeystoreTypes: string[];
clientSignatureSymmetricAlgorithms: string[];
clientSignatureAsymmetricAlgorithms: string[];
webAuthnSignatureAlgorithms: string[];
}
11 changes: 10 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@
</testframework.surefire.args>

<!-- webauthn support -->
<webauthn4j.version>0.30.3.RELEASE</webauthn4j.version>
<webauthn4j.version>0.32.0.RELEASE</webauthn4j.version>
<version.tools.jackson>3.2.4</version.tools.jackson>

<!-- Used to test SAML Galleon feature-pack layers discovery -->
<version.org.wildfly.glow>1.5.2.Final</version.org.wildfly.glow>
Expand Down Expand Up @@ -340,6 +341,14 @@
<dependencyManagement>

<dependencies>
<!-- Jackson 3 requires newer shared annotations than the Quarkus BOM provides. -->
<dependency>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${version.tools.jackson}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-bom</artifactId>
Expand Down
22 changes: 21 additions & 1 deletion quarkus/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,27 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
<exclusions>
<exclusion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ public void action(AuthenticationFlowContext context) {
context.getAuthenticationSession().removeAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE);
}
Challenge challenge = new DefaultChallenge(challengeNote);
ServerProperty server = new ServerProperty(origin, rpId, challenge);
ServerProperty server = ServerProperty.builder()
.origin(origin)
.rpId(rpId)
.challenge(challenge)
.build();

byte[] credentialId = Base64Url.decode(params.getFirst(WebAuthnConstants.CREDENTIAL_ID));
byte[] clientDataJSON = Base64Url.decode(params.getFirst(WebAuthnConstants.CLIENT_DATA_JSON));
Expand Down
Loading
Loading
0