Skip to content

Commit e1fb567

Browse files
authored
Merge pull request #481 from Yubico/ml-dsa
Support for ML-DSA
2 parents ad53360 + 73fc5ea commit e1fb567

17 files changed

Lines changed: 758 additions & 14 deletions

NEWS

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
== Version 2.10.0 (unreleased) ==
2+
3+
New features:
4+
5+
* Added support for ML-DSA-44, ML-DSA-65 and ML-DSA-87. This requires JRE 24 or
6+
later, or a third-party JCA provider. ML-DSA is enabled by default as the
7+
least preferred credential key algorithms; see
8+
`RelyingPartyBuilder.preferredPubkeyParams`.
9+
10+
111
== Version 2.9.0 ==
212

313
`webauthn-server-core`:

webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ public class RelyingParty {
216216
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256}
217217
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS384 RS384}
218218
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS512 RS512}
219+
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ML_DSA_44 ML_DSA_44}
220+
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ML_DSA_65 ML_DSA_65}
221+
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ML_DSA_87 ML_DSA_87}
219222
* </ol>
220223
*
221224
* @since 0.2.0
@@ -234,7 +237,10 @@ public class RelyingParty {
234237
PublicKeyCredentialParameters.Ed448,
235238
PublicKeyCredentialParameters.RS256,
236239
PublicKeyCredentialParameters.RS384,
237-
PublicKeyCredentialParameters.RS512));
240+
PublicKeyCredentialParameters.RS512,
241+
PublicKeyCredentialParameters.ML_DSA_44,
242+
PublicKeyCredentialParameters.ML_DSA_65,
243+
PublicKeyCredentialParameters.ML_DSA_87));
238244

239245
/**
240246
* If <code>true</code>, the origin matching rule is relaxed to allow any port number.
@@ -448,6 +454,18 @@ static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
448454
KeyFactory.getInstance("RSA");
449455
break;
450456

457+
case ML_DSA_44:
458+
KeyFactory.getInstance("ML-DSA-44");
459+
break;
460+
461+
case ML_DSA_65:
462+
KeyFactory.getInstance("ML-DSA-65");
463+
break;
464+
465+
case ML_DSA_87:
466+
KeyFactory.getInstance("ML-DSA-87");
467+
break;
468+
451469
default:
452470
log.warn(
453471
"Unknown algorithm: {}. Please file a bug report.", param.getAlg());

webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ public class RelyingPartyV2<C extends CredentialRecord> {
230230
* <li>{@link PublicKeyCredentialParameters#RS256 RS256}
231231
* <li>{@link PublicKeyCredentialParameters#RS384 RS384}
232232
* <li>{@link PublicKeyCredentialParameters#RS512 RS512}
233+
* <li>{@link PublicKeyCredentialParameters#ML_DSA_44 ML_DSA_44}
234+
* <li>{@link PublicKeyCredentialParameters#ML_DSA_65 ML_DSA_65}
235+
* <li>{@link PublicKeyCredentialParameters#ML_DSA_87 ML_DSA_87}
233236
* </ol>
234237
*
235238
* @see PublicKeyCredentialCreationOptions#getAttestation()
@@ -247,7 +250,10 @@ public class RelyingPartyV2<C extends CredentialRecord> {
247250
PublicKeyCredentialParameters.Ed448,
248251
PublicKeyCredentialParameters.RS256,
249252
PublicKeyCredentialParameters.RS384,
250-
PublicKeyCredentialParameters.RS512));
253+
PublicKeyCredentialParameters.RS512,
254+
PublicKeyCredentialParameters.ML_DSA_44,
255+
PublicKeyCredentialParameters.ML_DSA_65,
256+
PublicKeyCredentialParameters.ML_DSA_87));
251257

252258
/**
253259
* If <code>true</code>, the origin matching rule is relaxed to allow any port number.

webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,69 @@ final class WebAuthnCodecs {
8989
113
9090
});
9191

92+
static final ByteArray ML_DSA_44_ALG_ID =
93+
new ByteArray(
94+
new byte[] {
95+
// SEQUENCE (11 bytes)
96+
0x30,
97+
0x0B,
98+
// OID (9 bytes)
99+
0x06,
100+
0x09,
101+
// OID 2.16.840.1.101.3.4.3.17
102+
0x60,
103+
(byte) 0x86,
104+
0x48,
105+
0x01,
106+
0x65,
107+
0x03,
108+
0x04,
109+
0x03,
110+
0x11
111+
});
112+
113+
static final ByteArray ML_DSA_65_ALG_ID =
114+
new ByteArray(
115+
new byte[] {
116+
// SEQUENCE (11 bytes)
117+
0x30,
118+
0x0B,
119+
// OID (9 bytes)
120+
0x06,
121+
0x09,
122+
// OID 2.16.840.1.101.3.4.3.18
123+
0x60,
124+
(byte) 0x86,
125+
0x48,
126+
0x01,
127+
0x65,
128+
0x03,
129+
0x04,
130+
0x03,
131+
0x12
132+
});
133+
134+
static final ByteArray ML_DSA_87_ALG_ID =
135+
new ByteArray(
136+
new byte[] {
137+
// SEQUENCE (11 bytes)
138+
0x30,
139+
0x0B,
140+
// OID (9 bytes)
141+
0x06,
142+
0x09,
143+
// OID 2.16.840.1.101.3.4.3.19
144+
0x60,
145+
(byte) 0x86,
146+
0x48,
147+
0x01,
148+
0x65,
149+
0x03,
150+
0x04,
151+
0x03,
152+
0x13
153+
});
154+
92155
// See: https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
93156
static final int COSE_CRV_P256 = 1;
94157
static final int COSE_CRV_P384 = 2;
@@ -178,6 +241,8 @@ static PublicKey importCosePublicKey(ByteArray key)
178241
return importCoseEcdsaPublicKey(cose);
179242
case 3:
180243
return importCoseRsaPublicKey(cose);
244+
case 7:
245+
return importCoseMlDsaPublicKey(cose);
181246
default:
182247
throw new IllegalArgumentException("Unsupported key type: " + kty);
183248
}
@@ -263,6 +328,36 @@ private static ByteArray coseCurveToEddsaAlgorithmOid(int curveId) {
263328
}
264329
}
265330

331+
private static PublicKey importCoseMlDsaPublicKey(CBORObject cose)
332+
throws InvalidKeySpecException, NoSuchAlgorithmException {
333+
final int alg = cose.get(CBORObject.FromObject(3)).AsInt32();
334+
final ByteArray algorithmId = mlDsaAlgorithmId(alg);
335+
final byte[] rawKey = cose.get(CBORObject.FromObject(-1)).GetByteString();
336+
final byte[] x509Key =
337+
BinaryUtil.encodeDerSequence(
338+
algorithmId.getBytes(), BinaryUtil.encodeDerBitStringWithZeroUnused(rawKey));
339+
340+
KeyFactory kFact =
341+
KeyFactory.getInstance(
342+
getJavaAlgorithmName(
343+
COSEAlgorithmIdentifier.fromId(alg)
344+
.orElseThrow(() -> new IllegalArgumentException("Unknown algorithm: " + alg))));
345+
return kFact.generatePublic(new X509EncodedKeySpec(x509Key));
346+
}
347+
348+
private static ByteArray mlDsaAlgorithmId(int alg) {
349+
switch (alg) {
350+
case -48:
351+
return ML_DSA_44_ALG_ID;
352+
case -49:
353+
return ML_DSA_65_ALG_ID;
354+
case -50:
355+
return ML_DSA_87_ALG_ID;
356+
default:
357+
throw new IllegalArgumentException("Unsupported ML-DSA algorithm: " + alg);
358+
}
359+
}
360+
266361
static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
267362
switch (alg) {
268363
case EdDSA:
@@ -284,6 +379,12 @@ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
284379
return "SHA512withRSA";
285380
case RS1:
286381
return "SHA1withRSA";
382+
case ML_DSA_44:
383+
return "ML-DSA-44";
384+
case ML_DSA_65:
385+
return "ML-DSA-65";
386+
case ML_DSA_87:
387+
return "ML-DSA-87";
287388
default:
288389
throw new IllegalArgumentException("Unknown algorithm: " + alg);
289390
}

webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,40 @@ public enum COSEAlgorithmIdentifier {
176176
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
177177
* registry</a>
178178
*/
179-
RS1(-65535);
179+
RS1(-65535),
180+
181+
/**
182+
* ML-DSA-44 as defined in <a href="https://csrc.nist.gov/pubs/fips/204/final">NIST FIPS 204</a>.
183+
*
184+
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
185+
*
186+
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
187+
* registry</a>
188+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9964">RFC 9964</a>
189+
*/
190+
ML_DSA_44(-48),
191+
192+
/**
193+
* ML-DSA-65 as defined in <a href="https://csrc.nist.gov/pubs/fips/204/final">NIST FIPS 204</a>.
194+
*
195+
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
196+
*
197+
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
198+
* registry</a>
199+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9964">RFC 9964</a>
200+
*/
201+
ML_DSA_65(-49),
202+
203+
/**
204+
* ML-DSA-87 as defined in <a href="https://csrc.nist.gov/pubs/fips/204/final">NIST FIPS 204</a>.
205+
*
206+
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
207+
*
208+
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
209+
* registry</a>
210+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9964">RFC 9964</a>
211+
*/
212+
ML_DSA_87(-50);
180213

181214
@JsonValue @Getter private final long id;
182215

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,18 @@ private static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
543543
KeyFactory.getInstance("RSA");
544544
break;
545545

546+
case ML_DSA_44:
547+
KeyFactory.getInstance("ML-DSA-44");
548+
break;
549+
550+
case ML_DSA_65:
551+
KeyFactory.getInstance("ML-DSA-65");
552+
break;
553+
554+
case ML_DSA_87:
555+
KeyFactory.getInstance("ML-DSA-87");
556+
break;
557+
546558
default:
547559
log.warn(
548560
"Unknown algorithm: {}. Please file a bug report.", param.getAlg());
@@ -593,6 +605,18 @@ private static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
593605
Signature.getInstance("SHA1withRSA");
594606
break;
595607

608+
case ML_DSA_44:
609+
Signature.getInstance("ML-DSA-44");
610+
break;
611+
612+
case ML_DSA_65:
613+
Signature.getInstance("ML-DSA-65");
614+
break;
615+
616+
case ML_DSA_87:
617+
Signature.getInstance("ML-DSA-87");
618+
break;
619+
596620
default:
597621
log.warn(
598622
"Unknown algorithm: {}. Please file a bug report.", param.getAlg());

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,33 @@ private PublicKeyCredentialParameters(
146146
public static final PublicKeyCredentialParameters RS512 =
147147
builder().alg(COSEAlgorithmIdentifier.RS512).build();
148148

149+
/**
150+
* Algorithm {@link COSEAlgorithmIdentifier#ML_DSA_44} and type {@link
151+
* PublicKeyCredentialType#PUBLIC_KEY}.
152+
*
153+
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
154+
*/
155+
public static final PublicKeyCredentialParameters ML_DSA_44 =
156+
builder().alg(COSEAlgorithmIdentifier.ML_DSA_44).build();
157+
158+
/**
159+
* Algorithm {@link COSEAlgorithmIdentifier#ML_DSA_65} and type {@link
160+
* PublicKeyCredentialType#PUBLIC_KEY}.
161+
*
162+
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
163+
*/
164+
public static final PublicKeyCredentialParameters ML_DSA_65 =
165+
builder().alg(COSEAlgorithmIdentifier.ML_DSA_65).build();
166+
167+
/**
168+
* Algorithm {@link COSEAlgorithmIdentifier#ML_DSA_87} and type {@link
169+
* PublicKeyCredentialType#PUBLIC_KEY}.
170+
*
171+
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
172+
*/
173+
public static final PublicKeyCredentialParameters ML_DSA_87 =
174+
builder().alg(COSEAlgorithmIdentifier.ML_DSA_87).build();
175+
149176
public static PublicKeyCredentialParametersBuilder.MandatoryStages builder() {
150177
return new PublicKeyCredentialParametersBuilder.MandatoryStages();
151178
}

webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala

Lines changed: 130 additions & 0 deletions
Large diffs are not rendered by default.

webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import com.yubico.webauthn.extension.uvm.MatcherProtectionType
6060
import com.yubico.webauthn.extension.uvm.UserVerificationMethod
6161
import com.yubico.webauthn.test.Helpers
6262
import com.yubico.webauthn.test.RealExamples
63+
import com.yubico.webauthn.test.Util
6364
import com.yubico.webauthn.test.Util.toStepWithUtilities
6465
import org.junit.runner.RunWith
6566
import org.scalacheck.Arbitrary.arbitrary
@@ -2925,6 +2926,62 @@ class RelyingPartyAssertionSpec
29252926
)
29262927
}
29272928

2929+
for { algName <- List("ML-DSA-44", "ML-DSA-65", "ML-DSA-87") } it(
2930+
s"a generated ${algName} key, when available."
2931+
) {
2932+
assume(Util.algorithmAvailable(algName))
2933+
2934+
val registrationTestData = algName match {
2935+
case "ML-DSA-44" =>
2936+
RegistrationTestData.Packed.BasicAttestationMlDsa44
2937+
case "ML-DSA-65" =>
2938+
RegistrationTestData.Packed.BasicAttestationMlDsa65
2939+
case "ML-DSA-87" =>
2940+
RegistrationTestData.Packed.BasicAttestationMlDsa87
2941+
}
2942+
val testData = registrationTestData.assertion.get
2943+
2944+
val rp = RelyingParty
2945+
.builder()
2946+
.identity(
2947+
RelyingPartyIdentity.builder().id("localhost").name("Test RP").build()
2948+
)
2949+
.credentialRepository(
2950+
Helpers.CredentialRepository.withUser(
2951+
registrationTestData.userId,
2952+
RegisteredCredential
2953+
.builder()
2954+
.credentialId(registrationTestData.response.getId)
2955+
.userHandle(registrationTestData.userId.getId)
2956+
.publicKeyCose(
2957+
registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey
2958+
)
2959+
.signatureCount(0)
2960+
.build(),
2961+
)
2962+
)
2963+
.build()
2964+
2965+
val result = rp.finishAssertion(
2966+
FinishAssertionOptions
2967+
.builder()
2968+
.request(testData.request)
2969+
.response(testData.response)
2970+
.build()
2971+
)
2972+
2973+
result.isSuccess should be(true)
2974+
result.getCredential.getUserHandle should equal(
2975+
registrationTestData.userId.getId
2976+
)
2977+
result.getCredential.getCredentialId should equal(
2978+
registrationTestData.response.getId
2979+
)
2980+
result.getCredential.getCredentialId should equal(
2981+
testData.response.getId
2982+
)
2983+
}
2984+
29282985
describe("an RS1 key") {
29292986
def test(registrationTestData: RegistrationTestData): Unit = {
29302987
val testData = registrationTestData.assertion.get

0 commit comments

Comments
 (0)